virtus 1.0.1 → 2.0.0

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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +19 -15
  3. data/Changelog.md +43 -2
  4. data/Gemfile +5 -5
  5. data/README.md +113 -78
  6. data/Rakefile +13 -3
  7. data/lib/virtus.rb +46 -6
  8. data/lib/virtus/attribute.rb +21 -3
  9. data/lib/virtus/attribute/accessor.rb +11 -0
  10. data/lib/virtus/attribute/builder.rb +8 -13
  11. data/lib/virtus/attribute/collection.rb +12 -3
  12. data/lib/virtus/attribute/default_value.rb +2 -0
  13. data/lib/virtus/attribute/hash.rb +3 -3
  14. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  15. data/lib/virtus/attribute/strict.rb +1 -1
  16. data/lib/virtus/attribute_set.rb +2 -2
  17. data/lib/virtus/builder.rb +2 -6
  18. data/lib/virtus/class_inclusions.rb +0 -1
  19. data/lib/virtus/coercer.rb +1 -0
  20. data/lib/virtus/configuration.rb +17 -36
  21. data/lib/virtus/extensions.rb +13 -21
  22. data/lib/virtus/instance_methods.rb +3 -2
  23. data/lib/virtus/model.rb +1 -3
  24. data/lib/virtus/module_extensions.rb +8 -2
  25. data/lib/virtus/support/equalizer.rb +1 -1
  26. data/lib/virtus/support/options.rb +2 -1
  27. data/lib/virtus/support/type_lookup.rb +1 -1
  28. data/lib/virtus/version.rb +1 -1
  29. data/spec/integration/attributes_attribute_spec.rb +28 -0
  30. data/spec/integration/building_module_spec.rb +22 -0
  31. data/spec/integration/collection_member_coercion_spec.rb +34 -13
  32. data/spec/integration/custom_attributes_spec.rb +2 -2
  33. data/spec/integration/custom_collection_attributes_spec.rb +6 -6
  34. data/spec/integration/default_values_spec.rb +8 -8
  35. data/spec/integration/defining_attributes_spec.rb +25 -18
  36. data/spec/integration/embedded_value_spec.rb +5 -5
  37. data/spec/integration/extending_objects_spec.rb +5 -5
  38. data/spec/integration/hash_attributes_coercion_spec.rb +16 -12
  39. data/spec/integration/mass_assignment_with_accessors_spec.rb +5 -5
  40. data/spec/integration/overriding_virtus_spec.rb +4 -4
  41. data/spec/integration/required_attributes_spec.rb +1 -1
  42. data/spec/integration/struct_as_embedded_value_spec.rb +4 -4
  43. data/spec/integration/using_modules_spec.rb +8 -8
  44. data/spec/integration/value_object_with_custom_constructor_spec.rb +4 -4
  45. data/spec/integration/virtus/instance_level_attributes_spec.rb +1 -1
  46. data/spec/integration/virtus/value_object_spec.rb +14 -14
  47. data/spec/shared/freeze_method_behavior.rb +6 -3
  48. data/spec/shared/idempotent_method_behaviour.rb +1 -1
  49. data/spec/shared/options_class_method.rb +3 -3
  50. data/spec/spec_helper.rb +2 -18
  51. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +3 -3
  52. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +3 -3
  53. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +64 -24
  54. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +2 -2
  55. data/spec/unit/virtus/attribute/coerce_spec.rb +58 -10
  56. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +2 -2
  57. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +15 -4
  58. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +25 -4
  59. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  60. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  61. data/spec/unit/virtus/attribute/custom_collection_spec.rb +8 -2
  62. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  63. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +30 -15
  64. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +25 -11
  65. data/spec/unit/virtus/attribute/get_spec.rb +2 -2
  66. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +21 -9
  67. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +9 -9
  68. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +2 -2
  69. data/spec/unit/virtus/attribute/rename_spec.rb +6 -3
  70. data/spec/unit/virtus/attribute/required_predicate_spec.rb +2 -2
  71. data/spec/unit/virtus/attribute/set_default_value_spec.rb +43 -10
  72. data/spec/unit/virtus/attribute/set_spec.rb +1 -1
  73. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +2 -2
  74. data/spec/unit/virtus/attribute_set/append_spec.rb +6 -6
  75. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +12 -11
  76. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +13 -12
  77. data/spec/unit/virtus/attribute_set/each_spec.rb +21 -16
  78. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +2 -2
  79. data/spec/unit/virtus/attribute_set/element_set_spec.rb +17 -9
  80. data/spec/unit/virtus/attribute_set/merge_spec.rb +7 -5
  81. data/spec/unit/virtus/attribute_set/reset_spec.rb +22 -11
  82. data/spec/unit/virtus/attribute_spec.rb +8 -7
  83. data/spec/unit/virtus/attributes_reader_spec.rb +1 -1
  84. data/spec/unit/virtus/attributes_writer_spec.rb +1 -1
  85. data/spec/unit/virtus/element_reader_spec.rb +1 -1
  86. data/spec/unit/virtus/freeze_spec.rb +23 -3
  87. data/spec/unit/virtus/model_spec.rb +38 -7
  88. data/spec/unit/virtus/module_spec.rb +59 -2
  89. data/spec/unit/virtus/set_default_attributes_spec.rb +10 -3
  90. data/spec/unit/virtus/value_object_spec.rb +15 -5
  91. data/virtus.gemspec +7 -5
  92. metadata +46 -44
  93. data/.ruby-version +0 -1
  94. data/Gemfile.devtools +0 -54
  95. data/config/flay.yml +0 -3
  96. data/config/flog.yml +0 -2
  97. data/config/mutant.yml +0 -15
  98. data/config/reek.yml +0 -146
  99. data/config/yardstick.yml +0 -2
@@ -13,19 +13,20 @@ module Virtus
13
13
  # # strict mode
14
14
  # attr = Virtus::Attribute.build(Integer, :strict => true)
15
15
  # attr.coerce('not really coercible')
16
- # # => Virtus::CoercionError: Failed to coerce "fsafa" into Integer
16
+ # # => Virtus::CoercionError: Failed to coerce "not really coercible" into Integer
17
17
  #
18
18
  class Attribute
19
19
  extend DescendantsTracker, Options, TypeLookup
20
20
 
21
- include ::Equalizer.new(:type, :options)
21
+ include Equalizer.new(inspect) << :type << :options
22
22
 
23
- accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize
23
+ accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize, :nullify_blank
24
24
 
25
25
  strict false
26
26
  required true
27
27
  accessor :public
28
28
  finalize true
29
+ nullify_blank false
29
30
 
30
31
  # @see Virtus.coerce
31
32
  #
@@ -176,6 +177,23 @@ module Virtus
176
177
  kind_of?(Strict)
177
178
  end
178
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
+
179
197
  # Return if the attribute is accepts nil values as valid coercion output
180
198
  #
181
199
  # @example
@@ -34,6 +34,17 @@ module Virtus
34
34
  descendant.instance_variable_set('@instance_variable_name', "@#{name}")
35
35
  end
36
36
 
37
+ # Return if attribute value is defined
38
+ #
39
+ # @param [Object] instance
40
+ #
41
+ # @return [Boolean]
42
+ #
43
+ # @api public
44
+ def defined?(instance)
45
+ instance.instance_variable_defined?(instance_variable_name)
46
+ end
47
+
37
48
  # Return value of the attribute
38
49
  #
39
50
  # @param [Object] instance
@@ -25,13 +25,7 @@ module Virtus
25
25
  # @api private
26
26
  def determine_type
27
27
  if type.include?('::')
28
- # TODO: wrap it up in Virtus.constantize and use feature-detection to
29
- # pick up either Inflecto or ActiveSupport, whateve is available
30
- if defined?(Inflecto)
31
- Inflecto.constantize(type)
32
- else
33
- raise NotImplementedError, 'Virtus needs inflecto gem to constantize namespaced constant names'
34
- end
28
+ Virtus.constantize(type)
35
29
  else
36
30
  Object.const_get(type)
37
31
  end
@@ -53,7 +47,7 @@ module Virtus
53
47
 
54
48
  # @api private
55
49
  def pending?
56
- @pending
50
+ @pending if defined?(@pending)
57
51
  end
58
52
 
59
53
  private
@@ -107,7 +101,7 @@ module Virtus
107
101
  determine_type(klass.primitive)
108
102
  elsif EmbeddedValue.handles?(klass)
109
103
  EmbeddedValue
110
- elsif klass < Enumerable
104
+ elsif klass < Enumerable && !(klass <= Range)
111
105
  Collection
112
106
  end
113
107
  end
@@ -160,10 +154,11 @@ module Virtus
160
154
  def initialize_attribute
161
155
  @attribute = klass.new(type, options)
162
156
 
163
- @attribute.extend(Accessor) if options[:name]
164
- @attribute.extend(Coercible) if options[:coerce]
165
- @attribute.extend(Strict) if options[:strict]
166
- @attribute.extend(LazyDefault) if options[:lazy]
157
+ @attribute.extend(Accessor) if options[:name]
158
+ @attribute.extend(Coercible) if options[:coerce]
159
+ @attribute.extend(NullifyBlank) if options[:nullify_blank]
160
+ @attribute.extend(Strict) if options[:strict]
161
+ @attribute.extend(LazyDefault) if options[:lazy]
167
162
 
168
163
  @attribute.finalize if options[:finalize]
169
164
  end
@@ -66,16 +66,25 @@ module Virtus
66
66
 
67
67
  # @api private
68
68
  def self.merge_options!(type, options)
69
- options[:member_type] ||= Attribute.build(type.member_type)
69
+ options[:member_type] ||= Attribute.build(type.member_type, strict: options[:strict])
70
70
  end
71
71
 
72
72
  # @api public
73
- def coerce(*)
74
- super.each_with_object(primitive.new) do |entry, collection|
73
+ def coerce(value)
74
+ coerced = super
75
+
76
+ return coerced unless coerced.respond_to?(:each_with_object)
77
+
78
+ coerced.each_with_object(primitive.new) do |entry, collection|
75
79
  collection << member_type.coerce(entry)
76
80
  end
77
81
  end
78
82
 
83
+ # @api public
84
+ def value_coerced?(value)
85
+ super && value.all? { |item| member_type.value_coerced? item }
86
+ end
87
+
79
88
  # @api private
80
89
  def finalize
81
90
  return self if finalized?
@@ -7,6 +7,8 @@ module Virtus
7
7
  class DefaultValue
8
8
  extend DescendantsTracker
9
9
 
10
+ include Equalizer.new(inspect) << :value
11
+
10
12
  # Builds a default value instance
11
13
  #
12
14
  # @return [Virtus::Attribute::DefaultValue]
@@ -70,7 +70,7 @@ module Virtus
70
70
  value_type
71
71
  end
72
72
 
73
- { :key_type => key_primitive, :value_type => value_primitive}
73
+ { :key_type => key_primitive, :value_type => value_primitive}
74
74
  end
75
75
  end
76
76
 
@@ -92,8 +92,8 @@ module Virtus
92
92
 
93
93
  # @api private
94
94
  def self.merge_options!(type, options)
95
- options[:key_type] ||= Attribute.build(type.key_type)
96
- options[:value_type] ||= Attribute.build(type.value_type)
95
+ options[:key_type] ||= Attribute.build(type.key_type, :strict => options[:strict])
96
+ options[:value_type] ||= Attribute.build(type.value_type, :strict => options[:strict])
97
97
  end
98
98
 
99
99
  # Coerce members
@@ -0,0 +1,24 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Attribute extension which nullifies blank attributes when coercion failed
5
+ #
6
+ module NullifyBlank
7
+
8
+ # @see [Attribute#coerce]
9
+ #
10
+ # @api public
11
+ def coerce(input)
12
+ output = super
13
+
14
+ if !value_coerced?(output) && input.to_s.empty?
15
+ nil
16
+ else
17
+ output
18
+ end
19
+ end
20
+
21
+ end # NullifyBlank
22
+
23
+ end # Attribute
24
+ end # Virtus
@@ -16,7 +16,7 @@ module Virtus
16
16
  if value_coerced?(output) || !required? && output.nil?
17
17
  output
18
18
  else
19
- raise CoercionError.new(output, primitive)
19
+ raise CoercionError.new(output, self)
20
20
  end
21
21
  end
22
22
 
@@ -43,7 +43,7 @@ module Virtus
43
43
  # @api public
44
44
  def each
45
45
  return to_enum unless block_given?
46
- @index.values.uniq.each { |attribute| yield attribute }
46
+ @index.each { |name, attribute| yield attribute if name.kind_of?(Symbol) }
47
47
  self
48
48
  end
49
49
 
@@ -209,7 +209,7 @@ module Virtus
209
209
 
210
210
  # @api private
211
211
  def skip_default?(object, attribute)
212
- attribute.lazy? || object.instance_variable_defined?(attribute.instance_variable_name)
212
+ attribute.lazy? || attribute.defined?(object)
213
213
  end
214
214
 
215
215
  # Merge the attributes into the index
@@ -25,7 +25,7 @@ module Virtus
25
25
 
26
26
  # @api private
27
27
  def self.call(options, &block)
28
- new(Configuration.build(options, &block)).mod
28
+ new(Configuration.new(options, &block)).mod
29
29
  end
30
30
 
31
31
  # @api private
@@ -120,11 +120,7 @@ module Virtus
120
120
 
121
121
  # @api private
122
122
  def extensions
123
- super + [
124
- Extensions::AllowedWriterMethods,
125
- ValueObject::AllowedWriterMethods,
126
- ValueObject::InstanceMethods
127
- ]
123
+ super << ValueObject::AllowedWriterMethods << ValueObject::InstanceMethods
128
124
  end
129
125
 
130
126
  # @api private
@@ -13,7 +13,6 @@ module Virtus
13
13
  def self.included(descendant)
14
14
  super
15
15
  descendant.extend(ClassMethods)
16
- descendant.extend(Extensions::AllowedWriterMethods)
17
16
  descendant.class_eval { include Methods }
18
17
  descendant.class_eval { include InstanceMethods }
19
18
  descendant.class_eval { include InstanceMethods::Constructor }
@@ -3,6 +3,7 @@ module Virtus
3
3
  # Abstract coercer class
4
4
  #
5
5
  class Coercer
6
+ include Equalizer.new(inspect) << :primitive << :type
6
7
 
7
8
  # @api private
8
9
  attr_reader :primitive, :type
@@ -12,55 +12,34 @@ module Virtus
12
12
  # Access the strict setting for this instance
13
13
  attr_accessor :strict
14
14
 
15
+ # Access the nullify_blank setting for this instance
16
+ attr_accessor :nullify_blank
17
+
18
+ # Access the required setting for this instance
19
+ attr_accessor :required
20
+
15
21
  # Access the constructor setting for this instance
16
22
  attr_accessor :constructor
17
23
 
18
24
  # Access the mass-assignment setting for this instance
19
25
  attr_accessor :mass_assignment
20
26
 
21
- # Build new configuration instance using the passed block
22
- #
23
- # @example
24
- # Configuration.build do |config|
25
- # config.coerce = false
26
- # end
27
- #
28
- # @return [Configuration]
29
- #
30
- # @api public
31
- def self.build(options = {}, &block)
32
- config = new.call(&block)
33
- options.each { |key, value| config.public_send("#{key}=", value) }
34
- config
35
- end
36
-
37
27
  # Initialized a configuration instance
38
28
  #
39
29
  # @return [undefined]
40
30
  #
41
31
  # @api private
42
- def initialize
43
- @finalize = true
44
- @coerce = true
45
- @strict = false
46
- @constructor = true
47
- @mass_assignment = true
32
+ def initialize(options={})
33
+ @finalize = options.fetch(:finalize, true)
34
+ @coerce = options.fetch(:coerce, true)
35
+ @strict = options.fetch(:strict, false)
36
+ @nullify_blank = options.fetch(:nullify_blank, false)
37
+ @required = options.fetch(:required, true)
38
+ @constructor = options.fetch(:constructor, true)
39
+ @mass_assignment = options.fetch(:mass_assignment, true)
48
40
  @coercer = Coercible::Coercer.new
49
- end
50
41
 
51
- # Provide access to the attributes and methods via the passed block
52
- #
53
- # @example
54
- # configuration.call do |config|
55
- # config.coerce = false
56
- # end
57
- #
58
- # @return [self]
59
- #
60
- # @api private
61
- def call(&block)
62
- block.call(self) if block_given?
63
- self
42
+ yield self if block_given?
64
43
  end
65
44
 
66
45
  # Access the coercer for this instance and optional configure a
@@ -84,6 +63,8 @@ module Virtus
84
63
  { :coerce => coerce,
85
64
  :finalize => finalize,
86
65
  :strict => strict,
66
+ :nullify_blank => nullify_blank,
67
+ :required => required,
87
68
  :configured_coercer => coercer }.freeze
88
69
  end
89
70
 
@@ -18,7 +18,6 @@ module Virtus
18
18
  object.instance_eval do
19
19
  extend Methods
20
20
  extend InstanceMethods
21
- extend AllowedWriterMethods
22
21
  extend InstanceMethods::MassAssignment
23
22
  end
24
23
  end
@@ -72,26 +71,9 @@ module Virtus
72
71
  def values(&block)
73
72
  private :attributes= if instance_methods.include?(:attributes=)
74
73
  yield
75
- include(::Equalizer.new(*attribute_set.map(&:name)))
74
+ include(Equalizer.new(name, attribute_set.map(&:name)))
76
75
  end
77
76
 
78
- private
79
-
80
- # Return an attribute set for that instance
81
- #
82
- # @return [AttributeSet]
83
- #
84
- # @api private
85
- def attribute_set
86
- @attribute_set
87
- end
88
-
89
- end # Methods
90
-
91
- module AllowedWriterMethods
92
- WRITER_METHOD_REGEXP = /=\z/.freeze
93
- INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze
94
-
95
77
  # The list of writer methods that can be mass-assigned to in #attributes=
96
78
  #
97
79
  # @return [Set]
@@ -106,8 +88,18 @@ module Virtus
106
88
  end
107
89
  end
108
90
 
109
- end # AllowedWriterMethods
91
+ private
110
92
 
111
- end # module Extensions
93
+ # Return an attribute set for that instance
94
+ #
95
+ # @return [AttributeSet]
96
+ #
97
+ # @api private
98
+ def attribute_set
99
+ @attribute_set
100
+ end
112
101
 
102
+ end # Methods
103
+
104
+ end # module Extensions
113
105
  end # module Virtus
@@ -14,7 +14,7 @@ module Virtus
14
14
  #
15
15
  # @api private
16
16
  def initialize(attributes = nil)
17
- self.class.attribute_set.set(self, attributes) if attributes
17
+ attribute_set.set(self, attributes) if attributes
18
18
  set_default_attributes
19
19
  end
20
20
 
@@ -42,6 +42,7 @@ module Virtus
42
42
  attribute_set.get(self)
43
43
  end
44
44
  alias_method :to_hash, :attributes
45
+ alias_method :to_h, :attributes
45
46
 
46
47
  # Mass-assign attribute values
47
48
  #
@@ -191,7 +192,7 @@ module Virtus
191
192
  #
192
193
  # @api public
193
194
  def set_default_attributes!
194
- attribute_set.set_defaults(self, proc { |_| false })
195
+ attribute_set.set_defaults(self, proc { |object, attribute| attribute.defined?(object) })
195
196
  self
196
197
  end
197
198
 
data/lib/virtus/model.rb CHANGED
@@ -31,7 +31,7 @@ module Virtus
31
31
  descendant.extend(Extensions::Methods)
32
32
  descendant.extend(InstanceMethods)
33
33
  end
34
- private_class_method :included
34
+ private_class_method :extended
35
35
 
36
36
  end # Core
37
37
 
@@ -51,7 +51,6 @@ module Virtus
51
51
  # @api private
52
52
  def self.included(descendant)
53
53
  super
54
- descendant.extend(Extensions::AllowedWriterMethods)
55
54
  descendant.send(:include, InstanceMethods::MassAssignment)
56
55
  end
57
56
  private_class_method :included
@@ -59,7 +58,6 @@ module Virtus
59
58
  # @api private
60
59
  def self.extended(descendant)
61
60
  super
62
- descendant.extend(Extensions::AllowedWriterMethods)
63
61
  descendant.extend(InstanceMethods::MassAssignment)
64
62
  end
65
63
  private_class_method :extended