virtus2 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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