thin_models 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+