virtus 0.2.0 → 0.3.0

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