motion_virtus 1.0.0.beta0

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.
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