virtus 1.0.1 → 2.0.0

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