virtus 0.2.0 → 0.3.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 (54) hide show
  1. data/.pelusa.yml +7 -0
  2. data/.travis.yml +5 -3
  3. data/Changelog.md +17 -0
  4. data/Gemfile +4 -0
  5. data/README.md +35 -39
  6. data/TODO +12 -7
  7. data/config/flay.yml +1 -1
  8. data/config/flog.yml +1 -1
  9. data/lib/virtus.rb +8 -0
  10. data/lib/virtus/attribute.rb +8 -29
  11. data/lib/virtus/attribute/boolean.rb +1 -1
  12. data/lib/virtus/attribute/default_value.rb +15 -45
  13. data/lib/virtus/attribute/default_value/from_callable.rb +33 -0
  14. data/lib/virtus/attribute/default_value/from_clonable.rb +40 -0
  15. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  16. data/lib/virtus/attribute/embedded_value.rb +3 -14
  17. data/lib/virtus/class_methods.rb +17 -0
  18. data/lib/virtus/coercion/hash.rb +0 -11
  19. data/lib/virtus/coercion/object.rb +98 -0
  20. data/lib/virtus/coercion/string.rb +9 -2
  21. data/lib/virtus/coercion/time_coercions.rb +2 -5
  22. data/lib/virtus/instance_methods.rb +16 -37
  23. data/lib/virtus/support/type_lookup.rb +1 -2
  24. data/lib/virtus/value_object.rb +31 -8
  25. data/lib/virtus/value_object/equalizer.rb +21 -26
  26. data/lib/virtus/version.rb +1 -1
  27. data/spec/integration/custom_attributes_spec.rb +1 -1
  28. data/spec/integration/default_values_spec.rb +15 -3
  29. data/spec/integration/defining_attributes_spec.rb +1 -1
  30. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  31. data/spec/integration/virtus/value_object_spec.rb +2 -2
  32. data/spec/spec_helper.rb +8 -1
  33. data/spec/unit/virtus/attribute/class_methods/determine_type_spec.rb +1 -1
  34. data/spec/unit/virtus/attribute/default_spec.rb +1 -1
  35. data/spec/unit/virtus/attribute/default_value/evaluate_spec.rb +25 -11
  36. data/spec/unit/virtus/attribute/embedded_value/class_methods/merge_options_spec.rb +1 -1
  37. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +1 -1
  38. data/spec/unit/virtus/class_methods/allowed_writer_methods_spec.rb +25 -0
  39. data/spec/unit/virtus/coercion/object/class_methods/to_array_spec.rb +51 -0
  40. data/spec/unit/virtus/coercion/object/class_methods/to_hash_spec.rb +22 -0
  41. data/spec/unit/virtus/coercion/object/class_methods/to_integer_spec.rb +22 -0
  42. data/spec/unit/virtus/coercion/object/class_methods/to_string_spec.rb +22 -0
  43. data/spec/unit/virtus/coercion/string/class_methods/to_constant_spec.rb +37 -1
  44. data/spec/unit/virtus/instance_methods/attributes_spec.rb +14 -2
  45. data/spec/unit/virtus/value_object/class_methods/allowed_writer_methods_spec.rb +15 -0
  46. data/spec/unit/virtus/value_object/class_methods/equalizer_spec.rb +1 -1
  47. data/spec/unit/virtus/value_object/initialize_spec.rb +1 -1
  48. data/spec/unit/virtus/value_object/with_spec.rb +1 -1
  49. metadata +17 -10
  50. data/spec/unit/virtus/attribute/default_value/instance_methods/evaluate_spec.rb +0 -30
  51. data/spec/unit/virtus/attribute/instance_variable_name_spec.rb +0 -12
  52. data/spec/unit/virtus/attribute/reader_visibility_spec.rb +0 -24
  53. data/spec/unit/virtus/attribute/writer_visibility_spec.rb +0 -24
  54. data/spec/unit/virtus/coercion/hash/class_methods/to_array_spec.rb +0 -12
@@ -0,0 +1,33 @@
1
+ module Virtus
2
+ class Attribute
3
+ class DefaultValue
4
+
5
+ # Represents default value evaluated via a callable object
6
+ #
7
+ # @api private
8
+ class FromCallable < DefaultValue
9
+
10
+ # Return if the class can handle the value
11
+ #
12
+ # @return [Boolean]
13
+ #
14
+ # @api private
15
+ def self.handle?(attribute, value)
16
+ value.respond_to?(:call)
17
+ end
18
+
19
+ # Evaluates the value via value#call
20
+ #
21
+ # @param [Object]
22
+ #
23
+ # @return [Object] evaluated value
24
+ #
25
+ # @api private
26
+ def evaluate(instance)
27
+ @value.call(instance, @attribute)
28
+ end
29
+
30
+ end # class FromCallable
31
+ end # class DefaultValue
32
+ end # class Attribute
33
+ end # module Virtus
@@ -0,0 +1,40 @@
1
+ module Virtus
2
+ class Attribute
3
+ class DefaultValue
4
+
5
+ # Represents default value evaluated via a clonable object
6
+ #
7
+ # @api private
8
+ class FromClonable < DefaultValue
9
+ SINGLETON_CLASSES = [
10
+ ::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol ].freeze
11
+
12
+ # Return if the class can handle the value
13
+ #
14
+ # @return [Boolean]
15
+ #
16
+ # @api private
17
+ def self.handle?(attribute, value)
18
+ case value
19
+ when *SINGLETON_CLASSES
20
+ false
21
+ else
22
+ true
23
+ end
24
+ end
25
+
26
+ # Evaluates the value via value#clone
27
+ #
28
+ # @param [Object]
29
+ #
30
+ # @return [Object] evaluated value
31
+ #
32
+ # @api private
33
+ def evaluate(instance)
34
+ @value.clone
35
+ end
36
+
37
+ end # class FromClonable
38
+ end # class DefaultValue
39
+ end # class Attribute
40
+ end # module Virtus
@@ -0,0 +1,35 @@
1
+ module Virtus
2
+ class Attribute
3
+ class DefaultValue
4
+
5
+ # Represents default value evaluated via a symbol
6
+ #
7
+ # @api private
8
+ class FromSymbol < DefaultValue
9
+
10
+ # Return if the class can handle the value
11
+ #
12
+ # @return [Boolean]
13
+ #
14
+ # @api private
15
+ def self.handle?(attribute, value)
16
+ value.is_a?(::Symbol)
17
+ end
18
+
19
+ # Evaluates the value via instance#__send__(value)
20
+ #
21
+ # Symbol value is returned if the instance doesn't respond to value
22
+ #
23
+ # @param [Object]
24
+ #
25
+ # @return [Object] evaluated value
26
+ #
27
+ # @api private
28
+ def evaluate(instance)
29
+ instance.respond_to?(@value) ? instance.__send__(@value) : @value
30
+ end
31
+
32
+ end # class FromSymbol
33
+ end # class DefaultValue
34
+ end # class Attribute
35
+ end # module Virtus
@@ -25,6 +25,7 @@ module Virtus
25
25
  # :street => 'Street 1/2', :zipcode => '12345', :city => 'NYC' })
26
26
  #
27
27
  class EmbeddedValue < Object
28
+ primitive ::OpenStruct
28
29
 
29
30
  # @see Attribute.merge_options
30
31
  #
@@ -33,19 +34,7 @@ module Virtus
33
34
  #
34
35
  # @api private
35
36
  def self.merge_options(type, options)
36
- options.merge(:model => type)
37
- end
38
-
39
- # Sets @model ivar
40
- #
41
- # @see Virtus::Attribute#initialize
42
- #
43
- # @return [undefined]
44
- #
45
- # @api private
46
- def initialize(name, options = {})
47
- super
48
- @model = options.fetch(:model, OpenStruct)
37
+ options.merge(:primitive => type)
49
38
  end
50
39
 
51
40
  # Coerce attributes into a virtus object
@@ -57,7 +46,7 @@ module Virtus
57
46
  # @api private
58
47
  def coerce(attributes_or_object)
59
48
  value = if attributes_or_object.kind_of?(::Hash)
60
- @model.new(attributes_or_object)
49
+ @primitive.new(attributes_or_object)
61
50
  else
62
51
  attributes_or_object
63
52
  end
@@ -2,6 +2,8 @@ module Virtus
2
2
 
3
3
  # Class methods that are added when you include Virtus
4
4
  module ClassMethods
5
+ WRITER_METHOD_REGEXP = /=\z/.freeze
6
+ INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze
5
7
 
6
8
  # Hook called when module is extended
7
9
  #
@@ -78,6 +80,21 @@ module Virtus
78
80
  @attributes = AttributeSet.new(parent)
79
81
  end
80
82
 
83
+ # The list of writer methods that can be mass-assigned to in #attributes=
84
+ #
85
+ # @return [Set]
86
+ #
87
+ # @api private
88
+ def allowed_writer_methods
89
+ @allowed_writer_methods ||=
90
+ begin
91
+ allowed_writer_methods = public_instance_methods.map(&:to_s)
92
+ allowed_writer_methods = allowed_writer_methods.grep(WRITER_METHOD_REGEXP).to_set
93
+ allowed_writer_methods -= INVALID_WRITER_METHODS
94
+ allowed_writer_methods.freeze
95
+ end
96
+ end
97
+
81
98
  protected
82
99
 
83
100
  # Set up the anonymous module which will host Attribute accessor methods
@@ -7,17 +7,6 @@ module Virtus
7
7
 
8
8
  TIME_SEGMENTS = [ :year, :month, :day, :hour, :min, :sec ].freeze
9
9
 
10
- # Creates an Array instance from a Hash
11
- #
12
- # @param [Hash] value
13
- #
14
- # @return [Array]
15
- #
16
- # @api private
17
- def self.to_array(value)
18
- value.to_a
19
- end
20
-
21
10
  # Creates a Time instance from a Hash
22
11
  #
23
12
  # Valid keys are: :year, :month, :day, :hour, :min, :sec
@@ -7,6 +7,87 @@ module Virtus
7
7
 
8
8
  COERCION_METHOD_REGEXP = /\Ato_/.freeze
9
9
 
10
+ # Create an Array from any Object
11
+ #
12
+ # @example with an object that does not respond to #to_a or #to_ary
13
+ # Virtus::Coercion::Object.to_array(value) # => [ value ]
14
+ #
15
+ # @example with an object that responds to #to_a
16
+ # Virtus::Coercion::Object.to_array(Set[ value ]) # => [ value ]
17
+ #
18
+ # @example with n object that responds to #to_ary
19
+ # Virtus::Coercion::Object.to_array([ value ]) # => [ value ]
20
+ #
21
+ # @param [#to_a,#to_ary,Object] value
22
+ # @param [#to_a,#to_ary,Object] value
23
+ #
24
+ # @return [Array]
25
+ #
26
+ # @api public
27
+ def self.to_array(value)
28
+ Array(value)
29
+ end
30
+
31
+ # Create a Hash from the Object if possible
32
+ #
33
+ # @example with a coercible object
34
+ # Virtus::Coercion::Object.to_hash(key => value) # => { key => value }
35
+ #
36
+ # @example with an object that is not coercible
37
+ # Virtus::Coercion::Object.to_hash(value) # => value
38
+ #
39
+ # @param [#to_hash, Object] value
40
+ #
41
+ # @return [Hash]
42
+ # returns a Hash when the object can be coerced
43
+ # @return [Object]
44
+ # returns the value when the object cannot be coerced
45
+ #
46
+ # @api public
47
+ def self.to_hash(value)
48
+ coerce_with_method(value, :to_hash)
49
+ end
50
+
51
+ # Create a String from the Object if possible
52
+ #
53
+ # @example with a coercible object
54
+ # Virtus::Coercion::Object.to_string("string") # => "string"
55
+ #
56
+ # @example with an object that is not coercible
57
+ # Virtus::Coercion::Object.to_string(value) # => value
58
+ #
59
+ # @param [#to_str, Object] value
60
+ #
61
+ # @return [String]
62
+ # returns a String when the object can be coerced
63
+ # @return [Object]
64
+ # returns the value when the object cannot be coerced
65
+ #
66
+ # @api public
67
+ def self.to_string(value)
68
+ coerce_with_method(value, :to_str)
69
+ end
70
+
71
+ # Create an Integer from the Object if possible
72
+ #
73
+ # @example with a coercible object
74
+ # Virtus::Coercion::Object.to_integer(1) # => 1
75
+ #
76
+ # @example with an object that is not coercible
77
+ # Virtus::Coercion::Object.to_integer(value) # => value
78
+ #
79
+ # @param [#to_int, Object] value
80
+ #
81
+ # @return [Integer]
82
+ # returns an Integer when the object can be coerced
83
+ # @return [Object]
84
+ # returns the value when the object cannot be coerced
85
+ #
86
+ # @api public
87
+ def self.to_integer(value)
88
+ coerce_with_method(value, :to_int)
89
+ end
90
+
10
91
  # Passthrough given value
11
92
  #
12
93
  # @param [Object] value
@@ -22,6 +103,23 @@ module Virtus
22
103
  end
23
104
  end
24
105
 
106
+ private_class_method :method_missing
107
+
108
+ # Try to use native coercion method on the given value
109
+ #
110
+ # @param [Object] value
111
+ #
112
+ # @param [Symbol] method
113
+ #
114
+ # @return [Object]
115
+ #
116
+ # @api private
117
+ def self.coerce_with_method(value, method)
118
+ value.respond_to?(method) ? value.send(method) : value
119
+ end
120
+
121
+ private_class_method :coerce_with_method
122
+
25
123
  end # class Object
26
124
  end # class Coercion
27
125
  end # module Virtus
@@ -25,8 +25,15 @@ module Virtus
25
25
  #
26
26
  # @api public
27
27
  def self.to_constant(value)
28
- # TODO: add support for namespaced classes like 'Virtus::Attribute::String'
29
- ::Object.const_get(value)
28
+ names = value.split('::')
29
+ names.shift if names.first.empty?
30
+ names.inject(::Object) do |mod, name|
31
+ if mod.const_defined?(name, *EXTRA_CONST_ARGS)
32
+ mod.const_get(name, *EXTRA_CONST_ARGS)
33
+ else
34
+ mod.const_missing(name)
35
+ end
36
+ end
30
37
  end
31
38
 
32
39
  # Coerce give value to a symbol
@@ -73,11 +73,8 @@ module Virtus
73
73
  #
74
74
  # @api private
75
75
  def coerce_with_method(value, method)
76
- if value.respond_to?(method)
77
- value.send(method)
78
- else
79
- String.send(method, to_string(value))
80
- end
76
+ coerced = super
77
+ coerced.equal?(value) ? String.send(method, to_string(value)) : coerced
81
78
  end
82
79
 
83
80
  end # module TimeCoercions
@@ -82,17 +82,14 @@ module Virtus
82
82
  #
83
83
  # @api public
84
84
  def attributes
85
- self.class.attributes.each_with_object({}) do |attribute, attributes|
86
- name = attribute.name
87
- attributes[name] = self[name] if attribute.public_reader?
88
- end
85
+ get_attributes(&:public_reader?)
89
86
  end
90
87
 
91
88
  # Mass-assign attribute values
92
89
  #
93
90
  # Keys in the +attribute_values+ param can be symbols or strings.
94
- # Only non-private referenced Attribute writer methods will be called.
95
- # Non-attribute setter methods on the receiver will not be called.
91
+ # All referenced Attribute writer methods *will* be called.
92
+ # Non-attribute setter methods on the receiver *will* be called.
96
93
  #
97
94
  # @example
98
95
  # class User
@@ -112,11 +109,7 @@ module Virtus
112
109
  #
113
110
  # @api public
114
111
  def attributes=(attribute_values)
115
- attributes = self.class.attributes
116
- set_attributes(attribute_values.select { |name,|
117
- attribute = attributes[name]
118
- attribute && attribute.public_writer?
119
- })
112
+ set_attributes(attribute_values)
120
113
  end
121
114
 
122
115
  # Returns a hash of all publicly accessible attributes
@@ -141,42 +134,28 @@ module Virtus
141
134
 
142
135
  private
143
136
 
144
- # Mass-assign attribute values
145
- #
146
- # Keys in the +attribute_values+ param can be symbols or strings.
147
- # All referenced Attribute writer methods *will* be called.
148
- # Non-attribute setter methods on the receiver *will* be called.
149
- #
150
- # @example
151
- # class User
152
- # include Virtus
153
- #
154
- # attribute :name, String
155
- # attribute :age, Integer
156
- # end
157
- #
158
- # user = User.new
159
- # user.attributes = { :name => 'John', 'age' => 28 }
160
- #
161
- # @param [#to_hash] attribute_values
162
- # a hash of attribute names and values to set on the receiver
137
+ # Get values of all attributes defined for this class, ignoring privacy
163
138
  #
164
139
  # @return [Hash]
165
140
  #
166
141
  # @api private
167
- def set_attributes(attribute_values)
168
- attribute_values.each { |pair| set_attribute(*pair) }
142
+ def get_attributes
143
+ self.class.attributes.each_with_object({}) do |attribute, attributes|
144
+ name = attribute.name
145
+ attributes[name] = get_attribute(name) if yield(attribute)
146
+ end
169
147
  end
170
148
 
171
- # Get values of all attributes defined for this class, ignoring privacy
149
+ # Mass-assign attribute values
150
+ #
151
+ # @see Virtus::InstanceMethods#attributes=
172
152
  #
173
153
  # @return [Hash]
174
154
  #
175
155
  # @api private
176
- def get_attributes
177
- self.class.attributes.each_with_object({}) do |attribute, attributes|
178
- attribute_name = attribute.name
179
- attributes[attribute_name] = get_attribute(attribute_name)
156
+ def set_attributes(attribute_values)
157
+ attribute_values.each do |name, value|
158
+ set_attribute(name, value) if self.class.allowed_writer_methods.include?("#{name}=")
180
159
  end
181
160
  end
182
161
 
@@ -3,8 +3,7 @@ module Virtus
3
3
  # A module that adds type lookup to a class
4
4
  module TypeLookup
5
5
 
6
- TYPE_FORMAT = /\A[A-Z]\w*\z/.freeze
7
- EXTRA_CONST_ARGS = RUBY_VERSION < '1.9' || RUBY_ENGINE == 'rbx' ? [] : [ false ]
6
+ TYPE_FORMAT = /\A[A-Z]\w*\z/.freeze
8
7
 
9
8
  # Returns a descendant based on a name or class
10
9
  #
@@ -4,6 +4,7 @@ require 'virtus'
4
4
  require 'virtus/value_object/equalizer'
5
5
 
6
6
  module Virtus
7
+
7
8
  # Include this Module for Value Object semantics
8
9
  #
9
10
  # The idea is that instances should be immutable and compared based on state
@@ -22,6 +23,7 @@ module Virtus
22
23
  # hash = { location => :foo }
23
24
  # hash[same_location] #=> :foo
24
25
  module ValueObject
26
+
25
27
  # Callback to configure including Class as a Value Object
26
28
  #
27
29
  # Including Class will include Virtus and have additional
@@ -41,17 +43,25 @@ module Virtus
41
43
  end
42
44
  end
43
45
 
46
+ private_class_method :included
47
+
44
48
  module InstanceMethods
49
+ # the #get_attributes method accept a Proc object that will filter
50
+ # out an attribute when the block returns false. the ValueObject
51
+ # needs all the attributes, so we allow every attribute.
52
+ FILTER_NONE = proc { true }
53
+
45
54
  def initialize(attributes = {})
46
55
  set_attributes(attributes)
47
56
  end
48
57
 
49
58
  def with(attribute_updates)
50
- self.class.new(get_attributes.merge(attribute_updates))
59
+ self.class.new(get_attributes(&FILTER_NONE).merge(attribute_updates))
51
60
  end
52
61
  end
53
62
 
54
63
  module ClassMethods
64
+
55
65
  # Define an attribute on the receiver
56
66
  #
57
67
  # The Attribute will have private writer methods (eg., immutable instances)
@@ -73,7 +83,6 @@ module Virtus
73
83
  def attribute(name, type, options = {})
74
84
  equalizer << name
75
85
  options[:writer] = :private
76
-
77
86
  super
78
87
  end
79
88
 
@@ -91,14 +100,28 @@ module Virtus
91
100
  #
92
101
  # @api public
93
102
  def equalizer
94
- return @equalizer if instance_variable_defined?('@equalizer')
103
+ @equalizer ||=
104
+ begin
105
+ equalizer = Equalizer.new(name || inspect)
106
+ include equalizer
107
+ equalizer
108
+ end
109
+ end
95
110
 
96
- @equalizer = begin
97
- equalizer = Equalizer.new(name || inspect)
98
- include equalizer
99
- equalizer
100
- end
111
+ # The list of writer methods that can be mass-assigned to in #attributes=
112
+ #
113
+ # @return [Set]
114
+ #
115
+ # @api private
116
+ def allowed_writer_methods
117
+ @allowed_writer_methods ||=
118
+ begin
119
+ allowed_writer_methods = super
120
+ allowed_writer_methods += attributes.map{|attr| "#{attr.name}="}
121
+ allowed_writer_methods.to_set.freeze
122
+ end
101
123
  end
124
+
102
125
  end # module ClassMethods
103
126
  end # module ValueObject
104
127
  end # module Virtus