motion_virtus 1.0.0.beta0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +445 -0
  3. data/lib/motion_virtus.rb +13 -0
  4. data/lib/project/attribute/accessor/builder.rb +69 -0
  5. data/lib/project/attribute/accessor/lazy_accessor.rb +39 -0
  6. data/lib/project/attribute/accessor.rb +100 -0
  7. data/lib/project/attribute/accessor_method.rb +73 -0
  8. data/lib/project/attribute/array.rb +24 -0
  9. data/lib/project/attribute/boolean.rb +52 -0
  10. data/lib/project/attribute/class.rb +23 -0
  11. data/lib/project/attribute/coercer.rb +43 -0
  12. data/lib/project/attribute/collection/coercible_writer.rb +83 -0
  13. data/lib/project/attribute/collection.rb +56 -0
  14. data/lib/project/attribute/date.rb +36 -0
  15. data/lib/project/attribute/date_time.rb +38 -0
  16. data/lib/project/attribute/decimal.rb +23 -0
  17. data/lib/project/attribute/default_value/from_callable.rb +37 -0
  18. data/lib/project/attribute/default_value/from_clonable.rb +37 -0
  19. data/lib/project/attribute/default_value/from_symbol.rb +37 -0
  20. data/lib/project/attribute/default_value.rb +49 -0
  21. data/lib/project/attribute/embedded_value/open_struct_coercer.rb +43 -0
  22. data/lib/project/attribute/embedded_value/struct_coercer.rb +42 -0
  23. data/lib/project/attribute/embedded_value.rb +69 -0
  24. data/lib/project/attribute/float.rb +30 -0
  25. data/lib/project/attribute/hash/coercible_writer.rb +78 -0
  26. data/lib/project/attribute/hash.rb +66 -0
  27. data/lib/project/attribute/integer.rb +27 -0
  28. data/lib/project/attribute/numeric.rb +25 -0
  29. data/lib/project/attribute/object.rb +13 -0
  30. data/lib/project/attribute/reader.rb +39 -0
  31. data/lib/project/attribute/set.rb +22 -0
  32. data/lib/project/attribute/string.rb +24 -0
  33. data/lib/project/attribute/symbol.rb +23 -0
  34. data/lib/project/attribute/time.rb +36 -0
  35. data/lib/project/attribute/writer/coercible.rb +45 -0
  36. data/lib/project/attribute/writer.rb +73 -0
  37. data/lib/project/attribute.rb +292 -0
  38. data/lib/project/attribute_set.rb +260 -0
  39. data/lib/project/class_inclusions.rb +41 -0
  40. data/lib/project/class_methods.rb +102 -0
  41. data/lib/project/configuration.rb +65 -0
  42. data/lib/project/const_missing_extensions.rb +16 -0
  43. data/lib/project/extensions.rb +101 -0
  44. data/lib/project/instance_methods.rb +165 -0
  45. data/lib/project/module_builder.rb +92 -0
  46. data/lib/project/module_extensions.rb +72 -0
  47. data/lib/project/stubs/date.rb +2 -0
  48. data/lib/project/stubs/date_time.rb +2 -0
  49. data/lib/project/stubs/decimal.rb +2 -0
  50. data/lib/project/stubs/ostruct.rb +149 -0
  51. data/lib/project/stubs/set.rb +767 -0
  52. data/lib/project/stubs.rb +5 -0
  53. data/lib/project/support/equalizer.rb +147 -0
  54. data/lib/project/support/options.rb +114 -0
  55. data/lib/project/support/type_lookup.rb +109 -0
  56. data/lib/project/value_object.rb +139 -0
  57. data/lib/project/version.rb +3 -0
  58. data/lib/project/virtus.rb +128 -0
  59. metadata +158 -0
@@ -0,0 +1,292 @@
1
+ motion_require 'virtus'
2
+
3
+ module Virtus
4
+
5
+ # Abstract class implementing base API for attribute types
6
+ #
7
+ # @abstract
8
+ class Attribute
9
+ extend DescendantsTracker, TypeLookup, Options
10
+
11
+ #include Adamantium::Flat
12
+ include Equalizer.new(inspect) << :name
13
+
14
+ accept_options :primitive, :accessor, :reader,
15
+ :writer, :coercion_method, :default, :lazy
16
+
17
+ accessor :public
18
+
19
+ # @see Virtus.coerce
20
+ #
21
+ # @deprecated
22
+ #
23
+ # @api public
24
+ def self.coerce(value = Undefined)
25
+ warn "#{self}.coerce is deprecated and will be removed in a future version. Use Virtus.coerce instead: ##{caller.first}"
26
+ return Virtus.coerce if value.equal?(Undefined)
27
+ Virtus.coerce = value
28
+ self
29
+ end
30
+
31
+ # Returns name of the attribute
32
+ #
33
+ # @example
34
+ # User.attributes[:age].name # => :age
35
+ #
36
+ # @return [Symbol]
37
+ #
38
+ # @api public
39
+ attr_reader :name
40
+
41
+ # Return accessor object
42
+ #
43
+ # @return [Accessor]
44
+ #
45
+ # @api private
46
+ attr_reader :accessor
47
+
48
+ # Builds an attribute instance
49
+ #
50
+ # @param [Symbol] name
51
+ # the name of an attribute
52
+ #
53
+ # @param [Class] type
54
+ # optional type class of an attribute
55
+ #
56
+ # @param [#to_hash] options
57
+ # optional extra options hash
58
+ #
59
+ # @return [Attribute]
60
+ #
61
+ # @api private
62
+ def self.build(name, type = Object, options = {})
63
+ klass = determine_type(type) or raise(
64
+ ArgumentError, "#{type.inspect} does not map to an attribute type"
65
+ )
66
+
67
+ attribute_options = klass.merge_options(type, options)
68
+ accessor = Accessor.build(name, klass, attribute_options)
69
+
70
+ klass.new(name, accessor)
71
+ end
72
+
73
+ # Build coercer wrapper
74
+ #
75
+ # @example
76
+ #
77
+ # Virtus::Attribute.coercer # => #<Virtus::Attribute::Coercer ...>
78
+ #
79
+ # @return [Coercer]
80
+ #
81
+ # @api public
82
+ def self.coercer(type = nil, options = {})
83
+ coercer = options.fetch(:configured_coercer){ Virtus.coercer }
84
+ Coercer.new(coercer, coercion_method)
85
+ end
86
+
87
+ # Return default reader class
88
+ #
89
+ # @return [::Class]
90
+ #
91
+ # @api private
92
+ def self.reader_class(*)
93
+ Reader
94
+ end
95
+
96
+ # Return default writer class
97
+ #
98
+ # @param [::Class] attribute type
99
+ # @param [::Hash] attribute options
100
+ #
101
+ # @return [::Class]
102
+ #
103
+ # @api private
104
+ def self.writer_class(type, options)
105
+ coerce = options.fetch(:coerce){ Virtus.coerce }
106
+ coerce ? coercible_writer_class(type, options) : Writer
107
+ end
108
+
109
+ # Return default coercible writer class
110
+ #
111
+ # @param [::Class] attribute type
112
+ # @param [::Hash] attribute options
113
+ #
114
+ # @return [::Class]
115
+ #
116
+ # @api private
117
+ def self.coercible_writer_class(_type, _options)
118
+ Writer::Coercible
119
+ end
120
+
121
+ # Return default options for writer class
122
+ #
123
+ # @return [::Hash]
124
+ #
125
+ # @api private
126
+ def self.reader_options(*)
127
+ {}
128
+ end
129
+
130
+ # Return options accepted by writer class
131
+ #
132
+ # @return [Array<Symbol>]
133
+ #
134
+ # @api private
135
+ def self.writer_options(attribute_options)
136
+ ::Hash[writer_option_names.zip(attribute_options.values_at(*writer_option_names))]
137
+ end
138
+
139
+ # Return acceptable option names for write class
140
+ #
141
+ # @return [Array<Symbol>]
142
+ #
143
+ # @api private
144
+ def self.writer_option_names
145
+ [ :coercer, :primitive, :default ]
146
+ end
147
+
148
+ # Determine attribute type based on class or name
149
+ #
150
+ # Returns Attribute::EmbeddedValue if a virtus class is passed
151
+ #
152
+ # @example
153
+ # address_class = Class.new { include Virtus }
154
+ # Virtus::Attribute.determine_type(address_class) # => Virtus::Attribute::EmbeddedValue
155
+ #
156
+ # @see Virtus::Support::TypeLookup.determine_type
157
+ #
158
+ # @return [Class]
159
+ #
160
+ # @api public
161
+ def self.determine_type(class_or_name)
162
+ case class_or_name
163
+ when ::Class
164
+ EmbeddedValue.determine_type(class_or_name) or super
165
+ when ::String
166
+ super
167
+ when ::Enumerable
168
+ super(class_or_name.class)
169
+ else
170
+ super
171
+ end
172
+ end
173
+
174
+ # A hook for Attributes to update options based on the type from the caller
175
+ #
176
+ # @param [Object] type
177
+ # The raw type, typically given by the caller of ClassMethods#attribute
178
+ # @param [Hash] options
179
+ # Attribute configuration options
180
+ #
181
+ # @return [Hash]
182
+ # New Hash instance, potentially updated with information from the args
183
+ #
184
+ # @api private
185
+ def self.merge_options(type, options)
186
+ merged_options = self.options.merge(options)
187
+
188
+ if merged_options[:coerce]
189
+ merged_options.update(
190
+ :coercer => merged_options.fetch(:coercer) { coercer(type, options) }
191
+ )
192
+ end
193
+
194
+ merged_options
195
+ end
196
+
197
+ # Initializes an attribute instance
198
+ #
199
+ # @param [#to_sym] name
200
+ # the name of an attribute
201
+ #
202
+ # @param [#to_hash] options
203
+ # hash of extra options which overrides defaults set on an attribute class
204
+ #
205
+ # @return [undefined]
206
+ #
207
+ # @api private
208
+ def initialize(name, accessor)
209
+ @name = name.to_sym
210
+ @accessor = accessor
211
+ end
212
+
213
+ # Return reader object
214
+ #
215
+ # @example
216
+ #
217
+ # attribute.reader # => #<Virtus::Attribute::Reader ...>
218
+ #
219
+ # @return [Reader]
220
+ #
221
+ # @api public
222
+ def reader
223
+ accessor.reader
224
+ end
225
+
226
+ # Return writer object
227
+ #
228
+ # @example
229
+ #
230
+ # attribute.writer # => #<Virtus::Attribute::Writer ...>
231
+ #
232
+ # @return [Writer]
233
+ #
234
+ # @api public
235
+ def writer
236
+ accessor.writer
237
+ end
238
+
239
+ # Define reader and writer methods for an Attribute
240
+ #
241
+ # @param [AttributeSet] mod
242
+ #
243
+ # @return [self]
244
+ #
245
+ # @api private
246
+ def define_accessor_methods(attribute_set)
247
+ reader.define_method(accessor, attribute_set)
248
+ writer.define_method(accessor, attribute_set)
249
+ self
250
+ end
251
+
252
+ # Returns a Boolean indicating whether the reader method is public
253
+ #
254
+ # @return [Boolean]
255
+ #
256
+ # @api private
257
+ def public_reader?
258
+ accessor.public_reader?
259
+ end
260
+
261
+ # Returns a Boolean indicating whether the writer method is public
262
+ #
263
+ # @return [Boolean]
264
+ #
265
+ # @api private
266
+ def public_writer?
267
+ accessor.public_writer?
268
+ end
269
+
270
+ # Returns if the given value is coerced into the target type
271
+ #
272
+ # @return [Boolean]
273
+ #
274
+ # @api private
275
+ def value_coerced?(value)
276
+ coercer.coerced?(value)
277
+ end
278
+
279
+ private
280
+
281
+ # Return coercer for this attribute
282
+ #
283
+ # @return [Object]
284
+ #
285
+ # @api private
286
+ def coercer
287
+ writer.coercer[self.class.primitive]
288
+ end
289
+
290
+ end # class Attribute
291
+
292
+ end # module Virtus
@@ -0,0 +1,260 @@
1
+ module Virtus
2
+
3
+ class AS
4
+ include Enumerable
5
+
6
+ def initialize(parent = nil, attributes = [])
7
+ @parent = parent
8
+ @attributes = attributes
9
+ @index = {}
10
+ reset
11
+ end
12
+
13
+ def each
14
+ return to_enum unless block_given?
15
+ @index.values.uniq.each { |attribute| yield attribute }
16
+ self
17
+ end
18
+
19
+ end
20
+
21
+ module AttributeSet
22
+
23
+ def self.new(parent = nil, attributes = [])
24
+ Module.new.tap do |m|
25
+ m.module_eval do
26
+ extend Implementation
27
+ @parent = parent
28
+ @attributes = attributes
29
+ @index = {}
30
+ reset
31
+ end
32
+ end
33
+ end
34
+
35
+ module Implementation
36
+ include Enumerable
37
+
38
+ def inspect
39
+ "#<#{AttributeSet.name}:0x#{ '%x' % (object_id << 1) }>"
40
+ end
41
+
42
+ # Initialize an AttributeSet
43
+ #
44
+ # @param [AttributeSet] parent
45
+ # @param [Array] attributes
46
+ #
47
+ # @return [undefined]
48
+ #
49
+ # @api private
50
+ def initialize(parent = nil, attributes = [])
51
+ @parent = parent
52
+ @attributes = attributes.dup
53
+ @index = {}
54
+ reset
55
+ end
56
+
57
+ # Iterate over each attribute in the set
58
+ #
59
+ # @example
60
+ # attribute_set = AttributeSet.new(attributes, parent)
61
+ # attribute_set.each { |attribute| ... }
62
+ #
63
+ # @yield [attribute]
64
+ #
65
+ # @yieldparam [Attribute] attribute
66
+ # each attribute in the set
67
+ #
68
+ # @return [self]
69
+ #
70
+ # @api public
71
+ def each
72
+ return to_enum unless block_given?
73
+ @index.values.uniq.each { |attribute| yield attribute }
74
+ self
75
+ end
76
+
77
+ # Adds the attributes to the set
78
+ #
79
+ # @example
80
+ # attribute_set.merge(attributes)
81
+ #
82
+ # @param [Array<Attribute>] attributes
83
+ #
84
+ # @return [self]
85
+ #
86
+ # @api public
87
+ def merge(attributes)
88
+ attributes.each { |attribute| self << attribute }
89
+ self
90
+ end
91
+
92
+ # Adds an attribute to the set
93
+ #
94
+ # @example
95
+ # attribute_set << attribute
96
+ #
97
+ # @param [Attribute] attribute
98
+ #
99
+ # @return [self]
100
+ #
101
+ # @api public
102
+ def <<(attribute)
103
+ self[attribute.name] = attribute
104
+ attribute.define_accessor_methods(self)
105
+ self
106
+ end
107
+
108
+ # Get an attribute by name
109
+ #
110
+ # @example
111
+ # attribute_set[:name] # => Attribute object
112
+ #
113
+ # @param [Symbol] name
114
+ #
115
+ # @return [Attribute]
116
+ #
117
+ # @api public
118
+ def [](name)
119
+ @index[name]
120
+ end
121
+
122
+ # Set an attribute by name
123
+ #
124
+ # @example
125
+ # attribute_set[:name] = attribute
126
+ #
127
+ # @param [Symbol] name
128
+ # @param [Attribute] attribute
129
+ #
130
+ # @return [Attribute]
131
+ #
132
+ # @api public
133
+ def []=(name, attribute)
134
+ @attributes << attribute
135
+ update_index(name, attribute)
136
+ end
137
+
138
+ # Reset the index when the parent is updated
139
+ #
140
+ # @return [self]
141
+ #
142
+ # @api private
143
+ def reset
144
+ merge_attributes(@parent) if @parent
145
+ merge_attributes(@attributes)
146
+ self
147
+ end
148
+
149
+ # Defines an attribute reader method
150
+ #
151
+ # @param [Attribute] attribute
152
+ # @param [Symbol] method_name
153
+ # @param [Symbol] visibility
154
+ #
155
+ # @return [self]
156
+ #
157
+ # @api private
158
+ def define_reader_method(attribute, method_name, visibility)
159
+ define_method(method_name) { attribute.get(self) }
160
+ send(visibility, method_name)
161
+ self
162
+ end
163
+
164
+ # Defines an attribute writer method
165
+ #
166
+ # @param [Attribute] attribute
167
+ # @param [Symbol] method_name
168
+ # @param [Symbol] visibility
169
+ #
170
+ # @return [self]
171
+ #
172
+ # @api private
173
+ def define_writer_method(attribute, method_name, visibility)
174
+ define_method(method_name) { |value| attribute.set(self, value) }
175
+ send(visibility, method_name)
176
+ self
177
+ end
178
+
179
+ # Get values of all attributes defined for this class, ignoring privacy
180
+ #
181
+ # @return [Hash]
182
+ #
183
+ # @api private
184
+ def get(object, &block)
185
+ each_with_object({}) do |attribute, attributes|
186
+ name = attribute.name
187
+ attributes[name] = object.__send__(name) if yield(attribute)
188
+ end
189
+ end
190
+
191
+ # Mass-assign attribute values
192
+ #
193
+ # @see Virtus::InstanceMethods#attributes=
194
+ #
195
+ # @return [Hash]
196
+ #
197
+ # @api private
198
+ def set(object, attributes)
199
+ coerce(attributes).each do |name, value|
200
+ writer_name = "#{name}="
201
+ if object.allowed_writer_methods.include?(writer_name)
202
+ object.__send__(writer_name, value)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Set default attributes
208
+ #
209
+ # @return [self]
210
+ #
211
+ # @api private
212
+ def set_defaults(object)
213
+ each do |attribute|
214
+ if object.instance_variable_defined?(attribute.reader.instance_variable_name) || attribute.accessor.lazy?
215
+ next
216
+ end
217
+ attribute.writer.set_default_value(object, attribute)
218
+ end
219
+ end
220
+
221
+ # Coerce attributes received to a hash
222
+ #
223
+ # @return [Hash]
224
+ #
225
+ # @api private
226
+ def coerce(attributes)
227
+ ::Hash.try_convert(attributes) or raise(
228
+ NoMethodError, "Expected #{attributes.inspect} to respond to #to_hash"
229
+ )
230
+ end
231
+
232
+ private
233
+
234
+ # Merge the attributes into the index
235
+ #
236
+ # @param [Array<Attribute>] attributes
237
+ #
238
+ # @return [undefined]
239
+ #
240
+ # @api private
241
+ def merge_attributes(attributes)
242
+ attributes.each { |attribute| update_index(attribute.name, attribute) }
243
+ end
244
+
245
+ # Update the symbol and string indexes with the attribute
246
+ #
247
+ # @param [Symbol] name
248
+ #
249
+ # @param [Attribute] attribute
250
+ #
251
+ # @return [undefined]
252
+ #
253
+ # @api private
254
+ def update_index(name, attribute)
255
+ @index[name] = @index[name.to_s.freeze] = attribute
256
+ end
257
+
258
+ end # Implementation
259
+ end # AttributeSet
260
+ end # module Virtus
@@ -0,0 +1,41 @@
1
+ module Virtus
2
+
3
+ # Class-level extensions
4
+ module ClassInclusions
5
+
6
+ # Extends a descendant with class and instance methods
7
+ #
8
+ # @param [Class] descendant
9
+ #
10
+ # @return [undefined]
11
+ #
12
+ # @api private
13
+ def self.included(descendant)
14
+ super
15
+ descendant.extend(ClassMethods)
16
+ descendant.class_eval { include InstanceMethods }
17
+ end
18
+ private_class_method :included
19
+
20
+ # Return a list of allowed writer method names
21
+ #
22
+ # @return [Set]
23
+ #
24
+ # @api private
25
+ def allowed_writer_methods
26
+ self.class.allowed_writer_methods
27
+ end
28
+
29
+ private
30
+
31
+ # Return class' attribute set
32
+ #
33
+ # @return [Virtus::AttributeSet]
34
+ #
35
+ # @api private
36
+ def attribute_set
37
+ self.class.send(:attribute_set)
38
+ end
39
+
40
+ end # module ClassInclusions
41
+ end # module Virtus
@@ -0,0 +1,102 @@
1
+ module Virtus
2
+
3
+ # Class methods that are added when you include Virtus
4
+ module ClassMethods
5
+ include Extensions
6
+ include ConstMissingExtensions
7
+
8
+ # Hook called when module is extended
9
+ #
10
+ # @param [Class] descendant
11
+ #
12
+ # @return [undefined]
13
+ #
14
+ # @api private
15
+ def self.extended(descendant)
16
+ super
17
+ descendant.module_eval do
18
+ extend DescendantsTracker
19
+ include attribute_set
20
+ end
21
+ end
22
+
23
+ private_class_method :extended
24
+
25
+ # Returns all the attributes defined on a Class
26
+ #
27
+ # @example
28
+ # class User
29
+ # include Virtus
30
+ #
31
+ # attribute :name, String
32
+ # attribute :age, Integer
33
+ # end
34
+ #
35
+ # User.attribute_set # =>
36
+ #
37
+ # TODO: implement inspect so the output is not cluttered - solnic
38
+ #
39
+ # @return [AttributeSet]
40
+ #
41
+ # @api public
42
+ def attribute_set
43
+ return @attribute_set if defined?(@attribute_set)
44
+ superclass = self.superclass
45
+ method = __method__
46
+ parent = superclass.public_send(method) if superclass.respond_to?(method)
47
+ @attribute_set = AttributeSet.new(parent)
48
+ end
49
+
50
+ # @see Virtus::ClassMethods.attribute_set
51
+ #
52
+ # @deprecated
53
+ #
54
+ # @api public
55
+ def attributes
56
+ warn "#{self}.attributes is deprecated. Use #{self}.attribute_set instead: #{caller.first}"
57
+ attribute_set
58
+ end
59
+
60
+ private
61
+
62
+ # Setup descendants' own Attribute-accessor-method-hosting modules
63
+ #
64
+ # Descendants inherit Attribute accessor methods via Ruby's inheritance
65
+ # mechanism: Attribute accessor methods are defined in a module included
66
+ # in a superclass. Attributes defined on descendants add methods to the
67
+ # descendant's Attributes accessor module, leaving the superclass's method
68
+ # table unaffected.
69
+ #
70
+ # @param [Class] descendant
71
+ #
72
+ # @return [undefined]
73
+ #
74
+ # @api private
75
+ def inherited(descendant)
76
+ super
77
+ descendant.module_eval { include attribute_set }
78
+ end
79
+
80
+ # Add the attribute to the class' and descendants' attributes
81
+ #
82
+ # @param [Attribute] attribute
83
+ #
84
+ # @return [undefined]
85
+ #
86
+ # @api private
87
+ def virtus_add_attribute(attribute)
88
+ super
89
+ descendants.each { |descendant| descendant.attribute_set.reset }
90
+ end
91
+
92
+ # The list of allowed public methods
93
+ #
94
+ # @return [Array<String>]
95
+ #
96
+ # @api private
97
+ def allowed_methods
98
+ public_instance_methods.map(&:to_s)
99
+ end
100
+
101
+ end # module ClassMethods
102
+ end # module Virtus