virtus2 2.0.1

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 (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/CONTRIBUTING.md +18 -0
  6. data/Changelog.md +258 -0
  7. data/Gemfile +10 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +630 -0
  11. data/Rakefile +15 -0
  12. data/TODO.md +6 -0
  13. data/lib/virtus/attribute/accessor.rb +103 -0
  14. data/lib/virtus/attribute/boolean.rb +55 -0
  15. data/lib/virtus/attribute/builder.rb +182 -0
  16. data/lib/virtus/attribute/coercer.rb +45 -0
  17. data/lib/virtus/attribute/coercible.rb +20 -0
  18. data/lib/virtus/attribute/collection.rb +103 -0
  19. data/lib/virtus/attribute/default_value/from_callable.rb +35 -0
  20. data/lib/virtus/attribute/default_value/from_clonable.rb +35 -0
  21. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  22. data/lib/virtus/attribute/default_value.rb +51 -0
  23. data/lib/virtus/attribute/embedded_value.rb +67 -0
  24. data/lib/virtus/attribute/enum.rb +45 -0
  25. data/lib/virtus/attribute/hash.rb +130 -0
  26. data/lib/virtus/attribute/lazy_default.rb +18 -0
  27. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  28. data/lib/virtus/attribute/strict.rb +26 -0
  29. data/lib/virtus/attribute.rb +245 -0
  30. data/lib/virtus/attribute_set.rb +240 -0
  31. data/lib/virtus/builder/hook_context.rb +51 -0
  32. data/lib/virtus/builder.rb +133 -0
  33. data/lib/virtus/class_inclusions.rb +48 -0
  34. data/lib/virtus/class_methods.rb +90 -0
  35. data/lib/virtus/coercer.rb +41 -0
  36. data/lib/virtus/configuration.rb +72 -0
  37. data/lib/virtus/const_missing_extensions.rb +18 -0
  38. data/lib/virtus/extensions.rb +105 -0
  39. data/lib/virtus/instance_methods.rb +218 -0
  40. data/lib/virtus/model.rb +68 -0
  41. data/lib/virtus/module_extensions.rb +88 -0
  42. data/lib/virtus/support/equalizer.rb +128 -0
  43. data/lib/virtus/support/options.rb +113 -0
  44. data/lib/virtus/support/type_lookup.rb +109 -0
  45. data/lib/virtus/value_object.rb +150 -0
  46. data/lib/virtus/version.rb +3 -0
  47. data/lib/virtus.rb +310 -0
  48. data/spec/integration/attributes_attribute_spec.rb +28 -0
  49. data/spec/integration/building_module_spec.rb +90 -0
  50. data/spec/integration/collection_member_coercion_spec.rb +96 -0
  51. data/spec/integration/custom_attributes_spec.rb +42 -0
  52. data/spec/integration/custom_collection_attributes_spec.rb +101 -0
  53. data/spec/integration/default_values_spec.rb +87 -0
  54. data/spec/integration/defining_attributes_spec.rb +86 -0
  55. data/spec/integration/embedded_value_spec.rb +50 -0
  56. data/spec/integration/extending_objects_spec.rb +35 -0
  57. data/spec/integration/hash_attributes_coercion_spec.rb +54 -0
  58. data/spec/integration/inheritance_spec.rb +42 -0
  59. data/spec/integration/injectible_coercers_spec.rb +48 -0
  60. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  61. data/spec/integration/overriding_virtus_spec.rb +46 -0
  62. data/spec/integration/required_attributes_spec.rb +25 -0
  63. data/spec/integration/struct_as_embedded_value_spec.rb +28 -0
  64. data/spec/integration/using_modules_spec.rb +55 -0
  65. data/spec/integration/value_object_with_custom_constructor_spec.rb +42 -0
  66. data/spec/integration/virtus/instance_level_attributes_spec.rb +23 -0
  67. data/spec/integration/virtus/value_object_spec.rb +99 -0
  68. data/spec/shared/constants_helpers.rb +9 -0
  69. data/spec/shared/freeze_method_behavior.rb +40 -0
  70. data/spec/shared/idempotent_method_behaviour.rb +5 -0
  71. data/spec/shared/options_class_method.rb +19 -0
  72. data/spec/spec_helper.rb +41 -0
  73. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +43 -0
  74. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +25 -0
  75. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +180 -0
  76. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +32 -0
  77. data/spec/unit/virtus/attribute/coerce_spec.rb +129 -0
  78. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +20 -0
  79. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +105 -0
  80. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +74 -0
  81. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  82. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  83. data/spec/unit/virtus/attribute/custom_collection_spec.rb +29 -0
  84. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  85. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +70 -0
  86. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +91 -0
  87. data/spec/unit/virtus/attribute/get_spec.rb +32 -0
  88. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +106 -0
  89. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +92 -0
  90. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +20 -0
  91. data/spec/unit/virtus/attribute/rename_spec.rb +16 -0
  92. data/spec/unit/virtus/attribute/required_predicate_spec.rb +19 -0
  93. data/spec/unit/virtus/attribute/set_default_value_spec.rb +107 -0
  94. data/spec/unit/virtus/attribute/set_spec.rb +29 -0
  95. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +19 -0
  96. data/spec/unit/virtus/attribute_set/append_spec.rb +47 -0
  97. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +36 -0
  98. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +36 -0
  99. data/spec/unit/virtus/attribute_set/each_spec.rb +65 -0
  100. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +17 -0
  101. data/spec/unit/virtus/attribute_set/element_set_spec.rb +64 -0
  102. data/spec/unit/virtus/attribute_set/merge_spec.rb +34 -0
  103. data/spec/unit/virtus/attribute_set/reset_spec.rb +71 -0
  104. data/spec/unit/virtus/attribute_spec.rb +229 -0
  105. data/spec/unit/virtus/attributes_reader_spec.rb +41 -0
  106. data/spec/unit/virtus/attributes_writer_spec.rb +51 -0
  107. data/spec/unit/virtus/class_methods/finalize_spec.rb +67 -0
  108. data/spec/unit/virtus/class_methods/new_spec.rb +39 -0
  109. data/spec/unit/virtus/config_spec.rb +13 -0
  110. data/spec/unit/virtus/element_reader_spec.rb +21 -0
  111. data/spec/unit/virtus/element_writer_spec.rb +19 -0
  112. data/spec/unit/virtus/freeze_spec.rb +41 -0
  113. data/spec/unit/virtus/model_spec.rb +197 -0
  114. data/spec/unit/virtus/module_spec.rb +174 -0
  115. data/spec/unit/virtus/set_default_attributes_spec.rb +32 -0
  116. data/spec/unit/virtus/value_object_spec.rb +138 -0
  117. data/virtus2.gemspec +26 -0
  118. metadata +225 -0
@@ -0,0 +1,26 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Attribute extension which raises CoercionError when coercion failed
5
+ #
6
+ module Strict
7
+
8
+ # @see [Attribute#coerce]
9
+ #
10
+ # @raises [CoercionError] when coercer failed
11
+ #
12
+ # @api public
13
+ def coerce(*)
14
+ output = super
15
+
16
+ if value_coerced?(output) || !required? && output.nil?
17
+ output
18
+ else
19
+ raise CoercionError.new(output, self)
20
+ end
21
+ end
22
+
23
+ end # Strict
24
+
25
+ end # Attribute
26
+ end # Virtus
@@ -0,0 +1,245 @@
1
+ module Virtus
2
+
3
+ # Attribute objects handle coercion and provide interface to hook into an
4
+ # attribute set instance that's included into a class or object
5
+ #
6
+ # @example
7
+ #
8
+ # # non-strict mode
9
+ # attr = Virtus::Attribute.build(Integer)
10
+ # attr.coerce('1')
11
+ # # => 1
12
+ #
13
+ # # strict mode
14
+ # attr = Virtus::Attribute.build(Integer, :strict => true)
15
+ # attr.coerce('not really coercible')
16
+ # # => Virtus::CoercionError: Failed to coerce "not really coercible" into Integer
17
+ #
18
+ class Attribute
19
+ extend DescendantsTracker, Options, TypeLookup
20
+
21
+ include Equalizer.new(inspect) << :type << :options
22
+
23
+ accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize, :nullify_blank
24
+
25
+ strict false
26
+ required true
27
+ accessor :public
28
+ finalize true
29
+ nullify_blank false
30
+
31
+ # @see Virtus.coerce
32
+ #
33
+ # @deprecated
34
+ #
35
+ # @api public
36
+ def self.coerce(value = Undefined)
37
+ Virtus.warn "#{self}.coerce is deprecated and will be removed in 1.0.0. Use Virtus.coerce instead: ##{caller.first}"
38
+ return Virtus.coerce if value.equal?(Undefined)
39
+ Virtus.coerce = value
40
+ self
41
+ end
42
+
43
+ # Return type of this attribute
44
+ #
45
+ # @return [Axiom::Types::Type]
46
+ #
47
+ # @api public
48
+ attr_reader :type
49
+
50
+ # @api private
51
+ attr_reader :primitive, :options, :default_value, :coercer
52
+
53
+ # Builds an attribute instance
54
+ #
55
+ # @param [Class,Array,Hash,String,Symbol] type
56
+ # this can be an explicit class or an object from which virtus can infer
57
+ # the type
58
+ #
59
+ # @param [#to_hash] options
60
+ # optional extra options hash
61
+ #
62
+ # @return [Attribute]
63
+ #
64
+ # @api public
65
+ def self.build(type, options = {})
66
+ Builder.call(type, options)
67
+ end
68
+
69
+ # @api private
70
+ def self.build_coercer(type, options = {})
71
+ Coercer.new(type, options.fetch(:configured_coercer) { Virtus.coercer })
72
+ end
73
+
74
+ # @api private
75
+ def self.build_type(definition)
76
+ Axiom::Types.infer(definition.primitive)
77
+ end
78
+
79
+ # @api private
80
+ def self.merge_options!(*)
81
+ # noop
82
+ end
83
+
84
+ # @api private
85
+ def initialize(type, options)
86
+ @type = type
87
+ @primitive = type.primitive
88
+ @options = options
89
+ @default_value = options.fetch(:default_value)
90
+ @coercer = options.fetch(:coercer)
91
+ end
92
+
93
+ # Coerce the input into the expected type
94
+ #
95
+ # @example
96
+ #
97
+ # attr = Virtus::Attribute.build(String)
98
+ # attr.coerce(:one) # => 'one'
99
+ #
100
+ # @param [Object] input
101
+ #
102
+ # @api public
103
+ def coerce(input)
104
+ coercer.call(input)
105
+ end
106
+
107
+ # Return a new attribute with the new name
108
+ #
109
+ # @param [Symbol] name
110
+ #
111
+ # @return [Attribute]
112
+ #
113
+ # @api public
114
+ def rename(name)
115
+ self.class.build(type, options.merge(:name => name))
116
+ end
117
+
118
+ # Return if the given value was coerced
119
+ #
120
+ # @param [Object] value
121
+ #
122
+ # @return [Boolean]
123
+ #
124
+ # @api public
125
+ def value_coerced?(value)
126
+ coercer.success?(primitive, value)
127
+ end
128
+
129
+ # Return if the attribute is coercible
130
+ #
131
+ # @example
132
+ #
133
+ # attr = Virtus::Attribute.build(String, :coerce => true)
134
+ # attr.coercible? # => true
135
+ #
136
+ # attr = Virtus::Attribute.build(String, :coerce => false)
137
+ # attr.coercible? # => false
138
+ #
139
+ # @return [Boolean]
140
+ #
141
+ # @api public
142
+ def coercible?
143
+ kind_of?(Coercible)
144
+ end
145
+
146
+ # Return if the attribute has lazy default value evaluation
147
+ #
148
+ # @example
149
+ #
150
+ # attr = Virtus::Attribute.build(String, :lazy => true)
151
+ # attr.lazy? # => true
152
+ #
153
+ # attr = Virtus::Attribute.build(String, :lazy => false)
154
+ # attr.lazy? # => false
155
+ #
156
+ # @return [Boolean]
157
+ #
158
+ # @api public
159
+ def lazy?
160
+ kind_of?(LazyDefault)
161
+ end
162
+
163
+ # Return if the attribute is in the strict coercion mode
164
+ #
165
+ # @example
166
+ #
167
+ # attr = Virtus::Attribute.build(String, :strict => true)
168
+ # attr.strict? # => true
169
+ #
170
+ # attr = Virtus::Attribute.build(String, :strict => false)
171
+ # attr.strict? # => false
172
+ #
173
+ # @return [Boolean]
174
+ #
175
+ # @api public
176
+ def strict?
177
+ kind_of?(Strict)
178
+ end
179
+
180
+ # Return if the attribute is in the nullify blank coercion mode
181
+ #
182
+ # @example
183
+ #
184
+ # attr = Virtus::Attribute.build(String, :nullify_blank => true)
185
+ # attr.nullify_blank? # => true
186
+ #
187
+ # attr = Virtus::Attribute.build(String, :nullify_blank => false)
188
+ # attr.nullify_blank? # => false
189
+ #
190
+ # @return [Boolean]
191
+ #
192
+ # @api public
193
+ def nullify_blank?
194
+ kind_of?(NullifyBlank)
195
+ end
196
+
197
+ # Return if the attribute is accepts nil values as valid coercion output
198
+ #
199
+ # @example
200
+ #
201
+ # attr = Virtus::Attribute.build(String, :required => true)
202
+ # attr.required? # => true
203
+ #
204
+ # attr = Virtus::Attribute.build(String, :required => false)
205
+ # attr.required? # => false
206
+ #
207
+ # @return [Boolean]
208
+ #
209
+ # @api public
210
+ def required?
211
+ options[:required]
212
+ end
213
+
214
+ # Return if the attribute was already finalized
215
+ #
216
+ # @example
217
+ #
218
+ # attr = Virtus::Attribute.build(String, :finalize => true)
219
+ # attr.finalized? # => true
220
+ #
221
+ # attr = Virtus::Attribute.build(String, :finalize => false)
222
+ # attr.finalized? # => false
223
+ #
224
+ # @return [Boolean]
225
+ #
226
+ # @api public
227
+ def finalized?
228
+ frozen?
229
+ end
230
+
231
+ # @api private
232
+ def define_accessor_methods(attribute_set)
233
+ attribute_set.define_reader_method(self, name, options[:reader])
234
+ attribute_set.define_writer_method(self, "#{name}=", options[:writer])
235
+ end
236
+
237
+ # @api private
238
+ def finalize
239
+ freeze
240
+ self
241
+ end
242
+
243
+ end # class Attribute
244
+
245
+ end # module Virtus
@@ -0,0 +1,240 @@
1
+ module Virtus
2
+
3
+ # A set of Attribute objects
4
+ class AttributeSet < Module
5
+ include Enumerable
6
+
7
+ # @api private
8
+ def self.create(descendant)
9
+ if descendant.respond_to?(:superclass) && descendant.superclass.respond_to?(:attribute_set)
10
+ parent = descendant.superclass.public_send(:attribute_set)
11
+ end
12
+ descendant.instance_variable_set('@attribute_set', AttributeSet.new(parent))
13
+ end
14
+
15
+ # Initialize an AttributeSet
16
+ #
17
+ # @param [AttributeSet] parent
18
+ # @param [Array] attributes
19
+ #
20
+ # @return [undefined]
21
+ #
22
+ # @api private
23
+ def initialize(parent = nil, attributes = [])
24
+ @parent = parent
25
+ @attributes = attributes.dup
26
+ @index = {}
27
+ reset
28
+ end
29
+
30
+ # Iterate over each attribute in the set
31
+ #
32
+ # @example
33
+ # attribute_set = AttributeSet.new(attributes, parent)
34
+ # attribute_set.each { |attribute| ... }
35
+ #
36
+ # @yield [attribute]
37
+ #
38
+ # @yieldparam [Attribute] attribute
39
+ # each attribute in the set
40
+ #
41
+ # @return [self]
42
+ #
43
+ # @api public
44
+ def each
45
+ return to_enum unless block_given?
46
+ @index.each { |name, attribute| yield attribute if name.kind_of?(Symbol) }
47
+ self
48
+ end
49
+
50
+ # Adds the attributes to the set
51
+ #
52
+ # @example
53
+ # attribute_set.merge(attributes)
54
+ #
55
+ # @param [Array<Attribute>] attributes
56
+ #
57
+ # @return [self]
58
+ #
59
+ # @api public
60
+ def merge(attributes)
61
+ attributes.each { |attribute| self << attribute }
62
+ self
63
+ end
64
+
65
+ # Adds an attribute to the set
66
+ #
67
+ # @example
68
+ # attribute_set << attribute
69
+ #
70
+ # @param [Attribute] attribute
71
+ #
72
+ # @return [self]
73
+ #
74
+ # @api public
75
+ def <<(attribute)
76
+ self[attribute.name] = attribute
77
+ attribute.define_accessor_methods(self) if attribute.finalized?
78
+ self
79
+ end
80
+
81
+ # Get an attribute by name
82
+ #
83
+ # @example
84
+ # attribute_set[:name] # => Attribute object
85
+ #
86
+ # @param [Symbol] name
87
+ #
88
+ # @return [Attribute]
89
+ #
90
+ # @api public
91
+ def [](name)
92
+ @index[name]
93
+ end
94
+
95
+ # Set an attribute by name
96
+ #
97
+ # @example
98
+ # attribute_set[:name] = attribute
99
+ #
100
+ # @param [Symbol] name
101
+ # @param [Attribute] attribute
102
+ #
103
+ # @return [Attribute]
104
+ #
105
+ # @api public
106
+ def []=(name, attribute)
107
+ @attributes << attribute
108
+ update_index(name, attribute)
109
+ end
110
+
111
+ # Reset the index when the parent is updated
112
+ #
113
+ # @return [self]
114
+ #
115
+ # @api private
116
+ def reset
117
+ merge_attributes(@parent) if @parent
118
+ merge_attributes(@attributes)
119
+ self
120
+ end
121
+
122
+ # Defines an attribute reader method
123
+ #
124
+ # @param [Attribute] attribute
125
+ # @param [Symbol] method_name
126
+ # @param [Symbol] visibility
127
+ #
128
+ # @return [undefined]
129
+ #
130
+ # @api private
131
+ def define_reader_method(attribute, method_name, visibility)
132
+ define_method(method_name) { attribute.get(self) }
133
+ send(visibility, method_name)
134
+ end
135
+
136
+ # Defines an attribute writer method
137
+ #
138
+ # @param [Attribute] attribute
139
+ # @param [Symbol] method_name
140
+ # @param [Symbol] visibility
141
+ #
142
+ # @return [undefined]
143
+ #
144
+ # @api private
145
+ def define_writer_method(attribute, method_name, visibility)
146
+ define_method(method_name) { |value| attribute.set(self, value) }
147
+ send(visibility, method_name)
148
+ end
149
+
150
+ # Get values of all attributes defined for this class, ignoring privacy
151
+ #
152
+ # @return [Hash]
153
+ #
154
+ # @api private
155
+ def get(object)
156
+ each_with_object({}) do |attribute, attributes|
157
+ name = attribute.name
158
+ attributes[name] = object.__send__(name) if attribute.public_reader?
159
+ end
160
+ end
161
+
162
+ # Mass-assign attribute values
163
+ #
164
+ # @see Virtus::InstanceMethods#attributes=
165
+ #
166
+ # @return [Hash]
167
+ #
168
+ # @api private
169
+ def set(object, attributes)
170
+ coerce(attributes).each do |name, value|
171
+ writer_name = "#{name}="
172
+ if object.allowed_writer_methods.include?(writer_name)
173
+ object.__send__(writer_name, value)
174
+ end
175
+ end
176
+ end
177
+
178
+ # Set default attributes
179
+ #
180
+ # @return [self]
181
+ #
182
+ # @api private
183
+ def set_defaults(object, filter = method(:skip_default?))
184
+ each do |attribute|
185
+ next if filter.call(object, attribute)
186
+ attribute.set_default_value(object)
187
+ end
188
+ end
189
+
190
+ # Coerce attributes received to a hash
191
+ #
192
+ # @return [Hash]
193
+ #
194
+ # @api private
195
+ def coerce(attributes)
196
+ ::Hash.try_convert(attributes) or raise(
197
+ NoMethodError, "Expected #{attributes.inspect} to respond to #to_hash"
198
+ )
199
+ end
200
+
201
+ # @api private
202
+ def finalize
203
+ each do |attribute|
204
+ self << attribute.finalize unless attribute.finalized?
205
+ end
206
+ end
207
+
208
+ private
209
+
210
+ # @api private
211
+ def skip_default?(object, attribute)
212
+ attribute.lazy? || attribute.defined?(object)
213
+ end
214
+
215
+ # Merge the attributes into the index
216
+ #
217
+ # @param [Array<Attribute>] attributes
218
+ #
219
+ # @return [undefined]
220
+ #
221
+ # @api private
222
+ def merge_attributes(attributes)
223
+ attributes.each { |attribute| update_index(attribute.name, attribute) }
224
+ end
225
+
226
+ # Update the symbol and string indexes with the attribute
227
+ #
228
+ # @param [Symbol] name
229
+ #
230
+ # @param [Attribute] attribute
231
+ #
232
+ # @return [undefined]
233
+ #
234
+ # @api private
235
+ def update_index(name, attribute)
236
+ @index[name] = @index[name.to_s.freeze] = attribute
237
+ end
238
+
239
+ end # class AttributeSet
240
+ end # module Virtus