thin_models 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.txt ADDED
@@ -0,0 +1,4 @@
1
+ ThinModels
2
+ ==========
3
+
4
+ Some convenience classes for 'thin models' -- pure domain model data objects which are devoid of persistence and other infrastructural concerns.
File without changes
@@ -0,0 +1,3 @@
1
+ module ThinModels
2
+ class PartialDataError < StandardError; end
3
+ end
@@ -0,0 +1,226 @@
1
+ module ThinModels
2
+ # Exposes Enumerable and a subset of the interface of Array, but is lazily evaluated.
3
+ #
4
+ # The default constructor allows you to pass in an underlying Enumerable whose .each method will be
5
+ # used; or you can ignore this and override #each and #initialize yourself.
6
+ #
7
+ # You should also consider overriding #length if you have
8
+ # an optimised mechanism for evaluating it without doing a full iteration via #each, and
9
+ # overriding #slice_from_start_and_length if you have an optimised mechanism for iterating over a slice/sub-range of
10
+ # the array. This will be used to supply optimised versions of #[] / #slice
11
+ #
12
+ # Deliberately doesn't expose any mutation methods - is not intended to be a mutable data structure.
13
+ class LazyArray
14
+ include Enumerable
15
+
16
+ def initialize(enumerable=nil)
17
+ @enumerable = enumerable
18
+ end
19
+
20
+ def each(&b)
21
+ @enumerable.each(&b)
22
+ end
23
+
24
+ def inspect
25
+ "[ThinModels::LazyArray:...]"
26
+ end
27
+
28
+ # We recommend overriding this #length implementation (which is based on #each) with an efficient
29
+ # implementation. #size will use your #length, and #count uses #size where available, hence will
30
+ # use it too.
31
+ def length
32
+ length = 0; each {length += 1}; length
33
+ end
34
+
35
+ def size; length; end
36
+
37
+ def empty?
38
+ each {return false}
39
+ return true
40
+ end
41
+
42
+ def join(separator=$,)
43
+ result = ''; first = true
44
+ each do |x|
45
+ if first
46
+ first = false
47
+ else
48
+ result << separator if separator
49
+ end
50
+ result << x.to_s
51
+ end
52
+ result
53
+ end
54
+
55
+ # enables splat syntax: a, *b = lazy_array; foo(*lazy_array) etc.
56
+ alias :to_ary :to_a
57
+
58
+ def to_json(*p)
59
+ to_a.to_json(*p)
60
+ end
61
+
62
+ # We recommend overriding this inefficient implementation (which uses #each to traverse from
63
+ # the start until it reaches the desired range) with an efficient implementation.
64
+ #
65
+ # Returns an array for the requested slice; may return a slice shorter than requested where the
66
+ # array doesn't extend that far, but if the start index is greater than the total length, must return
67
+ # nil. This is consistent with Array#slice/[]
68
+ # eg: [][1..10] == nil, but [][0..10] == []
69
+ #
70
+ # Does not need to handle the other argument types (Range, single index) which Array#slice/[] takes.
71
+ def slice_from_start_and_length(start, length)
72
+ result = []
73
+ stop = start + length
74
+ index = 0
75
+ each do |item|
76
+ break if index >= stop
77
+ result << item if index >= start
78
+ index += 1
79
+ end
80
+ result if index >= start
81
+ end
82
+
83
+ # behaviour is consistent with Array#[], except it doesn't take negative indexes.
84
+ # uses slice_from_start_and_length to do the work.
85
+ def [](index_or_range, length=nil)
86
+ case index_or_range
87
+ when Range
88
+ start = index_or_range.begin
89
+ length = index_or_range.end - start
90
+ length += 1 unless index_or_range.exclude_end?
91
+ slice_from_start_and_length(start, length)
92
+ when Integer
93
+ if length
94
+ slice_from_start_and_length(index_or_range, length)
95
+ else
96
+ slice = slice_from_start_and_length(index_or_range, 1) and slice.first
97
+ end
98
+ else
99
+ raise ArgumentError
100
+ end
101
+ end
102
+
103
+ alias :slice :[]
104
+
105
+ def first
106
+ self[0]
107
+ end
108
+
109
+ def last
110
+ l = length
111
+ self[l-1] if l > 0
112
+ end
113
+
114
+ # map works lazily, resulting in a ThinModels::LazyArray::Mapped or a ThinModels::LazyArray::Memoized::Mapped (which additionally
115
+ # memoizes the mapped values)
116
+ def map(memoize=false, &b)
117
+ (memoize ? Memoized::Mapped : Mapped).new(self, &b)
118
+ end
119
+
120
+ class Mapped < ThinModels::LazyArray
121
+ def initialize(underlying, &block)
122
+ @underlying = underlying; @block = block
123
+ end
124
+
125
+ def each
126
+ @underlying.each {|x| yield @block.call(x)}
127
+ end
128
+
129
+ def length
130
+ @underlying.length
131
+ end
132
+
133
+ def slice_from_start_and_length(start, length)
134
+ @underlying.slice_from_start_and_length(start, length).map(&@block)
135
+ end
136
+ end
137
+
138
+ # Memoizes the #length of the array, but does not at present memoize the results of each or slice_from_start_and_length.
139
+ #
140
+ # #length will be memoized as a result of a direct call to #length (which uses an underlying #_length), or
141
+ # as a result of a full iteration via #each (which uses an underlying #_each)
142
+ #
143
+ # Your extension points are now #_each, #_length and #slice_from_start_and_length
144
+ class MemoizedLength < ThinModels::LazyArray
145
+ def each
146
+ length = 0
147
+ _each {|item| yield item; length += 1}
148
+ @length = length
149
+ self
150
+ end
151
+
152
+ alias :_length :length
153
+ def length
154
+ @length ||= _length
155
+ end
156
+
157
+ def inspect
158
+ if @length
159
+ "[ThinModels::LazyArray(length=#{@length}):...]"
160
+ else
161
+ "[ThinModels::LazyArray:...]"
162
+ end
163
+ end
164
+ end
165
+
166
+
167
+ # This additionally memoizes the full contents of the array once it's been fully each'd one time.
168
+ # The memoized full contents will then be used for future calls to #each (and hence all the other enumerable
169
+ # methods) and #[]. #to_a directly returns the memoized array once available.
170
+ #
171
+ # As with MemoizedLength, your extension points are now #_each, #_length and #slice_from_start_and_length
172
+ class Memoized < MemoizedLength
173
+ def each(&b)
174
+ if @to_a
175
+ @to_a.each(&b)
176
+ else
177
+ result = []
178
+ _each {|item| yield item; result << item}
179
+ @length = result.length
180
+ @to_a = result
181
+ end
182
+ self
183
+ end
184
+
185
+ def to_a
186
+ @to_a || super
187
+ end
188
+ alias :entries :to_a
189
+ alias :to_ary :to_a
190
+
191
+ def [](*p)
192
+ @to_a ? @to_a[*p] : super
193
+ end
194
+ alias :slice :[]
195
+
196
+ def inspect
197
+ if @to_ary
198
+ "[ThinModels::LazyArray: #{@to_ary.inspect[1..-1]}]"
199
+ elsif @length
200
+ "[ThinModels::LazyArray(length=#{@length}):...]"
201
+ else
202
+ "[ThinModels::LazyArray:...]"
203
+ end
204
+ end
205
+
206
+ # For when you want to map to a ThinModels::LazyArray which memoizes the results of the map
207
+ class Mapped < Memoized
208
+ def initialize(underlying, &block)
209
+ @underlying = underlying; @block = block
210
+ end
211
+
212
+ def _each
213
+ @underlying.each {|x| yield @block.call(x)}
214
+ end
215
+
216
+ def _length
217
+ @underlying.length
218
+ end
219
+
220
+ def slice_from_start_and_length(start, length)
221
+ @underlying.slice_from_start_and_length(start, length).map(&@block)
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,186 @@
1
+ require 'thin_models/errors'
2
+ require 'set'
3
+
4
+ module ThinModels
5
+
6
+ class Struct
7
+ def self.new_skipping_checks(values, &lazy_values)
8
+ new(values, true, &lazy_values)
9
+ end
10
+
11
+ def initialize(values=nil, skip_checks=false, &lazy_values)
12
+ @values = values || {}
13
+ @lazy_values = lazy_values if lazy_values
14
+ check_attributes if values && !skip_checks
15
+ end
16
+
17
+ def check_attributes
18
+ attributes = self.class.attributes
19
+ @values.each_key do |attribute|
20
+ raise NameError, "no attribute #{attribute} in #{self.class}" unless attributes.include?(attribute)
21
+ end
22
+ end
23
+
24
+ # this allows 'dup' to work in a desirable way for these instances,
25
+ # ie use a dup'd properties hash instance for the dup, meaning it can
26
+ # be updated without affecting the state of the original.
27
+ def initialize_copy(other)
28
+ super
29
+ @values = @values.dup
30
+ end
31
+
32
+ def freeze
33
+ super
34
+ @values.freeze
35
+ end
36
+
37
+ def loaded_values
38
+ @values.dup
39
+ end
40
+
41
+ # This helps these structs work with ruby methods, like merge, which expect a Hash.
42
+ alias :to_hash :loaded_values
43
+
44
+ def attribute_loaded?(attribute)
45
+ @values.has_key?(attribute)
46
+ end
47
+ alias :has_key? :attribute_loaded?
48
+
49
+ attr_accessor :lazy_values
50
+ private :lazy_values=
51
+
52
+ def remove_lazy_values
53
+ remove_instance_variable(:@lazy_values) if instance_variable_defined?(:@lazy_values)
54
+ end
55
+
56
+ def has_lazy_values?
57
+ instance_variable_defined?(:@lazy_values)
58
+ end
59
+
60
+ def attributes
61
+ self.class.attributes
62
+ end
63
+
64
+ def loaded_attributes
65
+ @values.keys
66
+ end
67
+ alias :keys :loaded_attributes
68
+
69
+ def [](attribute)
70
+ if @values.has_key?(attribute)
71
+ @values[attribute]
72
+ else
73
+ raise NameError, "no attribute #{attribute} in #{self.class}" unless self.class.attributes.include?(attribute)
74
+ if @lazy_values
75
+ @values[attribute] = @lazy_values.call(self, attribute)
76
+ end
77
+ end
78
+ end
79
+
80
+ def fetch(attribute)
81
+ if @values.has_key?(attribute)
82
+ @values[attribute]
83
+ else
84
+ raise NameError, "no attribute #{attribute} in #{self.class}" unless self.class.attributes.include?(attribute)
85
+ if @lazy_values
86
+ @values[attribute] = @lazy_values.call(self, attribute)
87
+ else
88
+ raise PartialDataError, "attribute #{attribute} not loaded"
89
+ end
90
+ end
91
+ end
92
+
93
+ def []=(attribute, value)
94
+ raise NameError, "no attribute #{attribute.inspect} in #{self.class}" unless self.class.attributes.include?(attribute)
95
+ @values[attribute] = value
96
+ end
97
+
98
+ def merge(updated_values)
99
+ dup.merge!(updated_values)
100
+ end
101
+
102
+ def merge!(updated_values)
103
+ updated_values.to_hash.each_key do |attribute|
104
+ raise NameError, "no attribute #{attribute.inspect} in #{self.class}" unless attributes.include?(attribute)
105
+ end
106
+ @values.merge!(updated_values)
107
+ self
108
+ end
109
+
110
+ # Based on Matz's code for OpenStruct#inspect in the stdlib.
111
+ #
112
+ # Note the trick with the Thread-local :__inspect_key__, which ruby internals appear to
113
+ # use but isn't documented anywhere. If you use it in the same way the stdlib uses it,
114
+ # you can override inspect without breaking its cycle avoidant behaviour
115
+ def inspect
116
+ str = "#<#{self.class}"
117
+
118
+ ids = (Thread.current[:__inspect_key__] ||= [])
119
+ if ids.include?(object_id)
120
+ return str << ' ...>'
121
+ end
122
+
123
+ ids << object_id
124
+ begin
125
+ first = true
126
+ for k,v in @values
127
+ str << "," unless first
128
+ first = false
129
+ str << " #{k}=#{v.inspect}"
130
+ end
131
+ if @lazy_values
132
+ str << "," unless first
133
+ str << " ..."
134
+ end
135
+ return str << '>'
136
+ ensure
137
+ ids.pop
138
+ end
139
+ end
140
+ alias :to_s :inspect
141
+
142
+ def to_json(*p)
143
+ @values.merge(:json_class => self.class).to_json(*p)
144
+ end
145
+
146
+ def self.json_create(json_values)
147
+ values = {}
148
+ attributes.each {|a| values[a] = json_values[a.to_s] if json_values.has_key?(a.to_s)}
149
+ new(values)
150
+ end
151
+
152
+
153
+ class << self
154
+ def attributes
155
+ @attributes ||= (superclass < Struct ? superclass.attributes.dup : Set.new)
156
+ end
157
+
158
+ private
159
+
160
+ def attribute(attribute)
161
+ raise "Attribute #{attribute} already defined on #{self}" if self.attributes.include?(attribute)
162
+ self.attributes << attribute
163
+ class_eval <<-EOS, __FILE__, __LINE__+1
164
+ def #{attribute}
165
+ fetch(#{attribute.inspect})
166
+ end
167
+ EOS
168
+ class_eval <<-EOS, __FILE__, __LINE__+1
169
+ def #{attribute}=(value)
170
+ self[#{attribute.inspect}] = value
171
+ end
172
+ EOS
173
+ end
174
+ end
175
+ end
176
+
177
+ # todo: add a ? to boolean getters
178
+
179
+ def self.Struct(*attributes)
180
+ Class.new(Struct) do
181
+ attributes.each {|a| attribute(a)}
182
+ end
183
+ end
184
+ end
185
+
186
+ require 'thin_models/struct/identity'
@@ -0,0 +1,42 @@
1
+ require 'thin_models/struct'
2
+
3
+ module ThinModels
4
+
5
+ module Struct::IdentityMethods
6
+ def ==(other)
7
+ super || (other.is_a?(self.class) && (id = self.id) && other.id == id) || false
8
+ end
9
+
10
+ def hash
11
+ id ? id.hash : super
12
+ end
13
+
14
+ # this was: alias :eql? :==, but that ran into http://redmine.ruby-lang.org/issues/show/734
15
+ def eql?(other)
16
+ super || (other.is_a?(self.class) && (id = self.id) && other.id == id) || false
17
+ end
18
+ end
19
+
20
+ class Struct
21
+ class << self
22
+ private
23
+ def identity_attribute(name=:id)
24
+ attribute(name) unless attributes.include?(name)
25
+ alias_method(:id=, "#{name}=") unless name == :id
26
+ class_eval <<-EOS, __FILE__, __LINE__+1
27
+ def id
28
+ @values[#{name.inspect}]
29
+ end
30
+ EOS
31
+ include IdentityMethods
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.StructWithIdentity(*attributes)
37
+ Class.new(Struct) do
38
+ identity_attribute
39
+ attributes.each {|a| attribute(a)}
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ require 'thin_models/struct'
2
+ begin
3
+ require 'typisch'
4
+ rescue LoadError
5
+ raise LoadError, "The typisch gem is required if you want to use thin_models/struct/typed"
6
+ end
7
+
8
+ class ThinModels::Struct::Typed < ThinModels::Struct
9
+ include Typisch::Typed
10
+
11
+ class << self
12
+ def type_available
13
+ type.property_names_to_types.map do |name, type|
14
+ attribute(name) unless attributes.include?(name) || method_defined?(name)
15
+ alias_method(:"#{name}?", name) if type.excluding_null.is_a?(Typisch::Type::Boolean)
16
+ end
17
+ end
18
+ end
19
+
20
+ # this will only type-check non-lazy properties -- not much point
21
+ # passing lazy properties if it's going to eagerly fetch them right
22
+ # away just to type-check them.
23
+ def check_attributes
24
+ self.class.type.property_names.each do |property|
25
+ type_check_property(property) if @values.has_key?(property)
26
+ end
27
+ end
28
+
29
+ def []=(attribute, value)
30
+ raise NameError, "no attribute #{attribute.inspect} in #{self.class}" unless self.class.attributes.include?(attribute)
31
+ @values[attribute] = value
32
+ type_check_property(attribute)
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module ThinModels
2
+ VERSION = "0.1.4"
3
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thin_models
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 4
10
+ version: 0.1.4
11
+ platform: ruby
12
+ authors:
13
+ - Matthew Willson
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-12 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: test-spec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: mocha
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: autotest
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: typisch
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ hash: 19
85
+ segments:
86
+ - 0
87
+ - 1
88
+ - 4
89
+ version: 0.1.4
90
+ type: :development
91
+ version_requirements: *id005
92
+ description:
93
+ email:
94
+ - matthew@playlouder.com
95
+ executables: []
96
+
97
+ extensions: []
98
+
99
+ extra_rdoc_files: []
100
+
101
+ files:
102
+ - lib/thin_models.rb
103
+ - lib/thin_models/version.rb
104
+ - lib/thin_models/struct.rb
105
+ - lib/thin_models/struct/typed.rb
106
+ - lib/thin_models/struct/identity.rb
107
+ - lib/thin_models/lazy_array.rb
108
+ - lib/thin_models/errors.rb
109
+ - README.txt
110
+ homepage:
111
+ licenses: []
112
+
113
+ post_install_message:
114
+ rdoc_options: []
115
+
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 3
124
+ segments:
125
+ - 0
126
+ version: "0"
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ hash: 3
133
+ segments:
134
+ - 0
135
+ version: "0"
136
+ requirements: []
137
+
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.10
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Some convenience classes for 'thin models' -- pure domain model data objects which are devoid of persistence and other infrastructural concerns
143
+ test_files: []
144
+