virtus 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,20 @@
1
- # v0.1.0 to-be-released
1
+ # v0.2.0 2012-02-08
2
2
 
3
- [Compare v0.0.10..master](https://github.com/solnic/virtus/compare/v0.0.10...master)
3
+ * [feature] Support for Value Objects (emmanuel)
4
+ * [feature] New Symbol attribute (solnic)
5
+ * [feature] Time => Integer coercion (solnic)
6
+
7
+ [Compare v0.1.0..v0.2.0](https://github.com/solnic/virtus/compare/v0.1.0...v0.2.0)
8
+
9
+ # v0.1.0 2012-02-05
4
10
 
5
11
  * [feature] New EmbeddedValue attribute (solnic)
12
+ * [feature] Array and Set attributes support member coercions (emmanuel)
6
13
  * [feature] Support for scientific notation handling in string => integer coercion (dkubb)
7
14
  * [feature] Handling of string => numeric coercion with a leading + sign (dkubb)
15
+ * [changed] Update Boolean coercion to handle "on", "off", "y", "n", "yes", "no" (dkubb)
8
16
 
17
+ [Compare v0.0.10..v0.1.0](https://github.com/solnic/virtus/compare/v0.0.10...v0.1.0)
9
18
 
10
19
  # v0.0.10 2011-11-21
11
20
 
data/README.md CHANGED
@@ -160,6 +160,39 @@ user.phone_numbers # => [#<PhoneNumber:0x007fdb2d3bef88 @number="212-555-1212">,
160
160
  user.addresses # => #<Set: {#<Address:0x007fdb2d3be448 @address="1234 Any St.", @locality="Anytown", @region="DC", @postal_code="21234">}>
161
161
  ```
162
162
 
163
+ **Value Objects**
164
+
165
+ ``` ruby
166
+ class GeoLocation
167
+ include Virtus::ValueObject
168
+
169
+ attribute :latitude, Float
170
+ attribute :longitude, Float
171
+ end
172
+
173
+ class Venue
174
+ include Virtus
175
+
176
+ attribute :name, String
177
+ attribute :location, GeoLocation
178
+ end
179
+
180
+ venue = Venue.new(
181
+ :name => 'Pub',
182
+ :location => { :latitude => 37.160317, :longitude => -98.437500 })
183
+
184
+ venue.location.latitude # => 37.160317
185
+ venue.location.longitude # => -98.4375
186
+
187
+ # Supports object's equality
188
+
189
+ venue_other = Venue.new(
190
+ :name => 'Other Pub',
191
+ :location => { :latitude => 37.160317, :longitude => -98.437500 })
192
+
193
+ venue.location === venue_other.location # => true
194
+ ```
195
+
163
196
  **Adding Coercions**
164
197
 
165
198
  Virtus comes with a builtin coercion library.
data/TODO CHANGED
@@ -1,8 +1,15 @@
1
1
  * Add missing specs:
2
- * Add spec file spec/unit/virtus/coercion/time_coercions/to_date_spec.rb for Virtus::Coercion::TimeCoercions#to_date
2
+ * Add spec file spec/unit/virtus/attribute/collection/member_coercion/coerce_and_append_member_spec.rb for Virtus::Attribute::Collection::MemberCoercion#coerce_and_append_member
3
+ * Add spec file spec/unit/virtus/attribute/collection/coerce_and_append_member_spec.rb for Virtus::Attribute::Collection#coerce_and_append_member
4
+ * Add spec file spec/unit/virtus/attribute/collection/member_type_spec.rb for Virtus::Attribute::Collection#member_type
5
+ * Add spec file spec/unit/virtus/attribute/collection/new_collection_spec.rb for Virtus::Attribute::Collection#new_collection
6
+ * Add spec file spec/unit/virtus/coercion/string/class_methods/to_symbol_spec.rb for Virtus::Coercion::String.to_symbol
7
+ * Add spec file spec/unit/virtus/coercion/time/class_methods/to_integer_spec.rb for Virtus::Coercion::Time.to_integer
3
8
  * Add spec file spec/unit/virtus/coercion/time_coercions/to_time_spec.rb for Virtus::Coercion::TimeCoercions#to_time
4
- * Add spec file spec/unit/virtus/coercion/time_coercions/to_string_spec.rb for Virtus::Coercion::TimeCoercions#to_string
5
9
  * Add spec file spec/unit/virtus/coercion/time_coercions/to_datetime_spec.rb for Virtus::Coercion::TimeCoercions#to_datetime
10
+ * Add spec file spec/unit/virtus/coercion/time_coercions/to_date_spec.rb for Virtus::Coercion::TimeCoercions#to_date
11
+ * Add spec file spec/unit/virtus/coercion/time_coercions/to_string_spec.rb for Virtus::Coercion::TimeCoercions#to_string
12
+ * Add spec file spec/unit/virtus/coercion/array/class_methods/to_set_spec.rb for Virtus::Coercion::Array.to_set
6
13
  * Add spec file spec/unit/virtus/coercion/decimal/class_methods/to_decimal_spec.rb for Virtus::Coercion::Decimal.to_decimal
7
14
  * Add spec file spec/unit/virtus/coercion/float/class_methods/to_float_spec.rb for Virtus::Coercion::Float.to_float
8
15
  * Add spec file spec/unit/virtus/coercion/integer/class_methods/to_integer_spec.rb for Virtus::Coercion::Integer.to_integer
@@ -12,9 +19,13 @@
12
19
  * Add spec file spec/unit/virtus/coercion/numeric/class_methods/to_decimal_spec.rb for Virtus::Coercion::Numeric.to_decimal
13
20
  * Add spec file spec/unit/virtus/coercion/class_methods/element_reference_spec.rb for Virtus::Coercion.[]
14
21
  * Add spec file spec/unit/virtus/coercion/class_methods/primitive_spec.rb for Virtus::Coercion.primitive
22
+ * Add spec file spec/unit/virtus/value_object/class_methods/attribute_spec.rb for Virtus::ValueObject::ClassMethods#attribute
23
+ * Add spec file spec/unit/virtus/value_object/instance_methods/with_spec.rb for Virtus::ValueObject::InstanceMethods#with
24
+ * Add spec file spec/unit/virtus/value_object/equalizer/compile_spec.rb for Virtus::ValueObject::Equalizer#compile
25
+ * Add spec file spec/unit/virtus/value_object/equalizer/append_spec.rb for Virtus::ValueObject::Equalizer#<<
26
+ * Add spec file spec/unit/virtus/value_object/equalizer/host_name_spec.rb for Virtus::ValueObject::Equalizer#host_name
27
+ * Add spec file spec/unit/virtus/value_object/equalizer/keys_spec.rb for Virtus::ValueObject::Equalizer#keys
15
28
  * Add spec file spec/unit/virtus/attributes_accessor/define_writer_method_spec.rb for Virtus::AttributesAccessor#define_writer_method
16
- * Add spec file spec/unit/virtus/attributes_accessor/inspect_spec.rb for Virtus::AttributesAccessor#inspect
17
29
  * Add spec file spec/unit/virtus/attributes_accessor/define_reader_method_spec.rb for Virtus::AttributesAccessor#define_reader_method
18
30
 
19
31
  * Make #to_time #to_date and #to_datetime work on Ruby 1.8.7 instead of typecasting to string and parsing the value
20
- * Add support for defining attributes on Modules
@@ -1,3 +1,3 @@
1
1
  ---
2
2
  threshold: 19
3
- total_score: 322
3
+ total_score: 354
@@ -2,16 +2,16 @@
2
2
  AbcMetricMethodCheck: { score: 12.1 }
3
3
  AssignmentInConditionalCheck: { }
4
4
  CaseMissingElseCheck: { }
5
- ClassLineCountCheck: { line_count: 319 }
5
+ ClassLineCountCheck: { line_count: 325 }
6
6
  ClassNameCheck: { pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ }
7
7
  ClassVariableCheck: { }
8
8
  CyclomaticComplexityBlockCheck: { complexity: 4 }
9
9
  CyclomaticComplexityMethodCheck: { complexity: 4 }
10
10
  EmptyRescueBodyCheck: { }
11
11
  ForLoopCheck: { }
12
- MethodLineCountCheck: { line_count: 9 }
12
+ MethodLineCountCheck: { line_count: 10 }
13
13
  MethodNameCheck: { pattern: !ruby/regexp /\A(?:[a-z\d](?:_?[a-z\d])+[?!=]?|\[\]=?|==|<=>|<<|[+*&|-])\z/ }
14
- ModuleLineCountCheck: { line_count: 325 }
14
+ ModuleLineCountCheck: { line_count: 331 }
15
15
  ModuleNameCheck: { pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ }
16
16
  # TODO: decrease parameter_count to 2 or less
17
17
  ParameterNumberCheck: { parameter_count: 3 }
@@ -35,6 +35,8 @@ require 'virtus/attributes_accessor'
35
35
  require 'virtus/class_methods'
36
36
  require 'virtus/instance_methods'
37
37
 
38
+ require 'virtus/value_object'
39
+
38
40
  require 'virtus/attribute_set'
39
41
 
40
42
  require 'virtus/coercion'
@@ -69,6 +71,7 @@ require 'virtus/attribute/decimal'
69
71
  require 'virtus/attribute/float'
70
72
  require 'virtus/attribute/hash'
71
73
  require 'virtus/attribute/integer'
74
+ require 'virtus/attribute/symbol'
72
75
  require 'virtus/attribute/string'
73
76
  require 'virtus/attribute/time'
74
77
  require 'virtus/attribute/embedded_value'
@@ -16,21 +16,7 @@ module Virtus
16
16
  primitive ::Array
17
17
  coercion_method :to_array
18
18
 
19
- # Coerce a member of a source collection and append it to the target collection
20
- #
21
- # @param [Array, Set] collection
22
- # target collection to which the coerced member should be appended
23
- #
24
- # @param [Object] entry
25
- # the member that should be coerced and appended
26
- #
27
- # @return [Array, Set]
28
- # collection with the coerced member appended to it
29
- #
30
- # @api private
31
- def coerce_and_append_member(collection, entry)
32
- collection << @member_type_instance.coerce(entry)
33
- end
19
+ include Collection::MemberCoercion
34
20
 
35
21
  end # class Array
36
22
  end # class Attribute
@@ -91,6 +91,26 @@ module Virtus
91
91
  "#{self.class}#coerce_and_append_member has not been implemented"
92
92
  end
93
93
 
94
+ # Default coercion method for collection members used by Array and Set
95
+ module MemberCoercion
96
+
97
+ # Coerce a member of a source collection and append it to the target collection
98
+ #
99
+ # @param [Array, Set] collection
100
+ # target collection to which the coerced member should be appended
101
+ #
102
+ # @param [Object] entry
103
+ # the member that should be coerced and appended
104
+ #
105
+ # @return [Array, Set]
106
+ # collection with the coerced member appended to it
107
+ #
108
+ # @api private
109
+ def coerce_and_append_member(collection, entry)
110
+ collection << @member_type_instance.coerce(entry)
111
+ end
112
+ end # module MemberCoercion
113
+
94
114
  end # class Array
95
115
  end # class Attribute
96
116
  end # module Virtus
@@ -16,9 +16,7 @@ module Virtus
16
16
  primitive ::Set
17
17
  coercion_method :to_set
18
18
 
19
- def coerce_and_append_member(collection, entry)
20
- collection << @member_type_instance.coerce(entry)
21
- end
19
+ include Collection::MemberCoercion
22
20
 
23
21
  end # class Set
24
22
  end # class Attribute
@@ -0,0 +1,21 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Symbol
5
+ #
6
+ # @example
7
+ # class Product
8
+ # include Virtus
9
+ #
10
+ # attribute :code, Symbol
11
+ # end
12
+ #
13
+ # product = Product.new(:code => :red)
14
+ #
15
+ class Symbol < Object
16
+ primitive ::Symbol
17
+ coercion_method :to_symbol
18
+
19
+ end # class Symbol
20
+ end # class Attribute
21
+ end # module Virtus
@@ -29,6 +29,20 @@ module Virtus
29
29
  ::Object.const_get(value)
30
30
  end
31
31
 
32
+ # Coerce give value to a symbol
33
+ #
34
+ # @example
35
+ # Virtus::Coercion::String.to_symbol('string') # => :string
36
+ #
37
+ # @param [String] value
38
+ #
39
+ # @return [Symbol]
40
+ #
41
+ # @api public
42
+ def self.to_symbol(value)
43
+ value.to_sym
44
+ end
45
+
32
46
  # Coerce given value to Time
33
47
  #
34
48
  # @example
@@ -21,6 +21,20 @@ module Virtus
21
21
  value
22
22
  end
23
23
 
24
+ # Creates a Fixnum instance from a Time object
25
+ #
26
+ # @example
27
+ # Virtus::Coercion::Time.to_integer(time) # => Fixnum object
28
+ #
29
+ # @param [Time] value
30
+ #
31
+ # @return [Fixnum]
32
+ #
33
+ # @api public
34
+ def self.to_integer(value)
35
+ value.to_i
36
+ end
37
+
24
38
  end # class Time
25
39
  end # class Coercion
26
40
  end # module Virtus
@@ -35,7 +35,7 @@ module Virtus
35
35
  #
36
36
  # @api public
37
37
  def [](name)
38
- __send__(name)
38
+ get_attribute(name)
39
39
  end
40
40
 
41
41
  # Sets a value of the attribute with the given name
@@ -62,7 +62,7 @@ module Virtus
62
62
  #
63
63
  # @api public
64
64
  def []=(name, value)
65
- __send__("#{name}=", value)
65
+ set_attribute(name, value)
66
66
  end
67
67
 
68
68
  # Returns a hash of all publicly accessible attributes
@@ -165,7 +165,41 @@ module Virtus
165
165
  #
166
166
  # @api private
167
167
  def set_attributes(attribute_values)
168
- attribute_values.each { |name, value| self[name] = value }
168
+ attribute_values.each { |pair| set_attribute(*pair) }
169
+ end
170
+
171
+ # Get values of all attributes defined for this class, ignoring privacy
172
+ #
173
+ # @return [Hash]
174
+ #
175
+ # @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)
180
+ end
181
+ end
182
+
183
+ # Returns a value of the attribute with the given name
184
+ #
185
+ # @see Virtus::InstanceMethods#[]
186
+ #
187
+ # @return [Object]
188
+ #
189
+ # @api private
190
+ def get_attribute(name)
191
+ __send__(name)
192
+ end
193
+
194
+ # Sets a value of the attribute with the given name
195
+ #
196
+ # @see Virtus::InstanceMethods#[]=
197
+ #
198
+ # @return [Object]
199
+ #
200
+ # @api private
201
+ def set_attribute(name, value)
202
+ __send__("#{name}=", value)
169
203
  end
170
204
 
171
205
  end # module InstanceMethods
@@ -0,0 +1,104 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'virtus'
4
+ require 'virtus/value_object/equalizer'
5
+
6
+ module Virtus
7
+ # Include this Module for Value Object semantics
8
+ #
9
+ # The idea is that instances should be immutable and compared based on state
10
+ # (rather than identity, as is typically the case)
11
+ #
12
+ # @example
13
+ # class GeoLocation
14
+ # include Virtus::ValueObject
15
+ # attribute :latitude, Float
16
+ # attribute :longitude, Float
17
+ # end
18
+ #
19
+ # location = GeoLocation.new(:latitude => 10, :longitude => 100)
20
+ # same_location = GeoLocation.new(:latitude => 10, :longitude => 100)
21
+ # location == same_location #=> true
22
+ # hash = { location => :foo }
23
+ # hash[same_location] #=> :foo
24
+ module ValueObject
25
+ # Callback to configure including Class as a Value Object
26
+ #
27
+ # Including Class will include Virtus and have additional
28
+ # value object semantics defined in this module
29
+ #
30
+ # @return [Undefined]
31
+ #
32
+ # TODO: stacking modules is getting painful
33
+ # time for Facets' module_inheritance, ActiveSupport::Concern or the like
34
+ #
35
+ # @api private
36
+ def self.included(base)
37
+ base.instance_eval do
38
+ include ::Virtus
39
+ include InstanceMethods
40
+ extend ClassMethods
41
+ end
42
+ end
43
+
44
+ module InstanceMethods
45
+ def initialize(attributes = {})
46
+ set_attributes(attributes)
47
+ end
48
+
49
+ def with(attribute_updates)
50
+ self.class.new(get_attributes.merge(attribute_updates))
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+ # Define an attribute on the receiver
56
+ #
57
+ # The Attribute will have private writer methods (eg., immutable instances)
58
+ # and be used in equality/equivalence comparisons
59
+ #
60
+ # @example
61
+ # class GeoLocation
62
+ # include Virtus::ValueObject
63
+ #
64
+ # attribute :latitude, Float
65
+ # attribute :longitude, Float
66
+ # end
67
+ #
68
+ # @see Virtus::ClassMethods.attribute
69
+ #
70
+ # @return [self]
71
+ #
72
+ # @api public
73
+ def attribute(name, type, options = {})
74
+ equalizer << name
75
+ options[:writer] = :private
76
+
77
+ super
78
+ end
79
+
80
+ # Define and include a module that provides Value Object semantics
81
+ #
82
+ # Included module will have #inspect, #eql?, #== and #hash
83
+ # methods whose definition is based on the _keys_ argument
84
+ #
85
+ # @example
86
+ # virtus_class.equalizer
87
+ #
88
+ # @return [Equalizer]
89
+ # An Equalizer module which defines #inspect, #eql?, #== and #hash
90
+ # for instances of this class
91
+ #
92
+ # @api public
93
+ def equalizer
94
+ return @equalizer if instance_variable_defined?('@equalizer')
95
+
96
+ @equalizer = begin
97
+ equalizer = Equalizer.new(name || inspect)
98
+ include equalizer
99
+ equalizer
100
+ end
101
+ end
102
+ end # module ClassMethods
103
+ end # module ValueObject
104
+ end # module Virtus
@@ -0,0 +1,129 @@
1
+ module Virtus
2
+ module ValueObject
3
+ # A type of Module for dynamically defining and hosting equality methods
4
+ class Equalizer < Module
5
+ # Name of hosting Class or Module that will be used for #inspect
6
+ #
7
+ # @return [String]
8
+ #
9
+ # @api private
10
+ attr_reader :host_name
11
+
12
+ # List of methods that will be used to define equality methods
13
+ #
14
+ # @return [Array(Symbol)]
15
+ #
16
+ # @api private
17
+ attr_reader :keys
18
+
19
+ # Initialize an Equalizer with the given keys
20
+ #
21
+ # Will use the keys with which it is initialized to define #eql?, #==,
22
+ # and #hash
23
+ #
24
+ # @api private
25
+ def initialize(host_name, keys = [])
26
+ @host_name = host_name
27
+ @keys = keys
28
+ end
29
+
30
+ # Append a key and compile the equality methods
31
+ #
32
+ # @return [Equalizer] self
33
+ #
34
+ # @api private
35
+ def <<(key)
36
+ @keys << key
37
+ compile
38
+
39
+ self
40
+ end
41
+
42
+ # Compile the equalizer methods based on #keys
43
+ #
44
+ # @return [self]
45
+ #
46
+ # @api private
47
+ def compile
48
+ define_inspect_method
49
+ define_eql_method
50
+ define_equivalent_method
51
+ define_hash_method
52
+
53
+ self
54
+ end
55
+
56
+ private
57
+
58
+ # Define an inspect method that reports the values of the instance's keys
59
+ #
60
+ # @return [self]
61
+ #
62
+ # @api private
63
+ def define_inspect_method
64
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
65
+ def inspect
66
+ "#<#{host_name} #{keys.map { |key| "#{key}=\#{#{key}.inspect}" }.join(' ')}>"
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ # Define an #eql? method based on the instance's values identified by #keys
72
+ #
73
+ # @return [self]
74
+ #
75
+ # @api private
76
+ def define_eql_method
77
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
78
+ def eql?(other)
79
+ return true if equal?(other)
80
+ instance_of?(other.class) &&
81
+ #{keys.map { |key| "#{key}.eql?(other.#{key})" }.join(' && ')}
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ # Define an #== method based on the instance's values identified by #keys
87
+ #
88
+ # @return [self]
89
+ #
90
+ # @api private
91
+ def define_equivalent_method
92
+ respond_to, equivalent = compile_strings_for_equivalent_method
93
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
94
+ def ==(other)
95
+ return true if equal?(other)
96
+ return false unless kind_of?(other.class) || other.kind_of?(self.class)
97
+ #{respond_to.join(' && ')} && #{equivalent.join(' && ')}
98
+ end
99
+ RUBY
100
+ end
101
+
102
+ # Define a #hash method based on the instance's values identified by #keys
103
+ #
104
+ # @return [self]
105
+ #
106
+ # @api private
107
+ def define_hash_method
108
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
109
+ def hash
110
+ self.class.hash ^ #{keys.map { |key| "#{key}.hash" }.join(' ^ ')}
111
+ end
112
+ RUBY
113
+ end
114
+
115
+ # @api private
116
+ def compile_strings_for_equivalent_method
117
+ respond_to = []
118
+ equivalent = []
119
+
120
+ keys.each do |key|
121
+ respond_to << "other.respond_to?(#{key.inspect})"
122
+ equivalent << "#{key} == other.#{key}"
123
+ end
124
+
125
+ [ respond_to, equivalent ]
126
+ end
127
+ end # class Equalizer
128
+ end # module ValueObject
129
+ end # module Virtus
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -72,4 +72,4 @@ describe User do
72
72
  its(:region) { should eql('DC') }
73
73
  its(:postal_code) { should eql('21234') }
74
74
  end
75
- end
75
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject do
4
+ let(:class_under_test) do
5
+ Class.new do
6
+ def self.name
7
+ 'GeoLocation'
8
+ end
9
+
10
+ include Virtus::ValueObject
11
+
12
+ attribute :latitude, Float
13
+ attribute :longitude, Float
14
+ end
15
+ end
16
+ let(:attribute_values) { { :latitude => 10.0, :longitude => 20.0 } }
17
+ let(:instance_with_equal_state) { class_under_test.new(attribute_values) }
18
+ let(:instance_with_different_state) do
19
+ class_under_test.new(:latitude => attribute_values[:latitude])
20
+ end
21
+ subject { class_under_test.new(attribute_values) }
22
+
23
+ describe 'initialization' do
24
+ it 'sets the attribute values provided to Class.new' do
25
+ class_under_test.new(:latitude => 10000.001).latitude.should == 10000.001
26
+ subject.latitude.should eql(attribute_values[:latitude])
27
+ end
28
+ end
29
+
30
+ describe 'writer visibility' do
31
+ it 'attributes are configured for private writers' do
32
+ class_under_test.attributes[:latitude].writer_visibility.should == :private
33
+ class_under_test.attributes[:longitude].writer_visibility.should == :private
34
+ end
35
+
36
+ it 'writer methods are set to private' do
37
+ private_methods = class_under_test.private_instance_methods
38
+ private_methods.map! { |m| m.to_s }
39
+ private_methods.should include('latitude=', 'longitude=')
40
+ end
41
+
42
+ it 'attempts to call attribute writer methods raises NameError' do
43
+ expect { subject.latitude = 5.0 }.to raise_exception(NameError)
44
+ expect { subject.longitude = 5.0 }.to raise_exception(NameError)
45
+ end
46
+ end
47
+
48
+ describe 'equality' do
49
+ describe '#==' do
50
+ it 'returns true for different objects with the same state' do
51
+ subject.should == instance_with_equal_state
52
+ end
53
+
54
+ it 'returns false for different objects with different state' do
55
+ subject.should_not == instance_with_different_state
56
+ end
57
+ end
58
+
59
+ describe '#eql?' do
60
+ it 'returns true for different objects with the same state' do
61
+ subject.should eql(instance_with_equal_state)
62
+ end
63
+
64
+ it 'returns false for different objects with different state' do
65
+ subject.should_not eql(instance_with_different_state)
66
+ end
67
+ end
68
+
69
+ describe '#equal?' do
70
+ it 'returns false for different objects with the same state' do
71
+ subject.should_not equal(instance_with_equal_state)
72
+ end
73
+
74
+ it 'returns false for different objects with different state' do
75
+ subject.should_not equal(instance_with_different_state)
76
+ end
77
+ end
78
+
79
+ describe '#hash' do
80
+ it 'returns the same value for different objects with the same state' do
81
+ subject.hash.should eql(instance_with_equal_state.hash)
82
+ end
83
+
84
+ it 'returns different values for different objects with different state' do
85
+ subject.hash.should_not eql(instance_with_different_state.hash)
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#inspect' do
91
+ it 'includes the class name and attribute values' do
92
+ subject.inspect.should == '#<GeoLocation latitude=10.0 longitude=20.0>'
93
+ end
94
+ end
95
+ end
@@ -18,7 +18,7 @@ describe Virtus::Attribute, '.determine_type' do
18
18
 
19
19
  before do
20
20
  if [Virtus::Attribute::EmbeddedValue, Virtus::Attribute::Collection].include? attribute_class
21
- pending
21
+ pending
22
22
  end
23
23
  end
24
24
 
@@ -26,7 +26,7 @@ describe Virtus::Attribute::Collection, '.merge_options' do
26
26
  context 'when size is > 1' do
27
27
  let(:size) { 2 }
28
28
 
29
- specify { expect { subject }.to raise_error(NotImplementedError) }
29
+ specify { expect { subject }.to raise_error(NotImplementedError, "build SumType from list of types (#{type.inspect})") }
30
30
  end
31
31
  end
32
32
 
@@ -37,4 +37,4 @@ describe Virtus::Attribute::Collection, '.merge_options' do
37
37
 
38
38
  it { should eql(options) }
39
39
  end
40
- end
40
+ end
@@ -11,10 +11,10 @@ describe Virtus::Attribute::Collection, '#coerce' do
11
11
  before do
12
12
  value.should_receive(:respond_to?).with(:inject).and_return(respond_to_inject)
13
13
  end
14
-
14
+
15
15
  context 'when coerced value responds to #inject' do
16
16
  let(:respond_to_inject) { true }
17
-
17
+
18
18
  specify { expect { subject }.to raise_error(NotImplementedError) }
19
19
  end
20
20
 
@@ -23,4 +23,4 @@ describe Virtus::Attribute::Collection, '#coerce' do
23
23
 
24
24
  specify { should eql(value) }
25
25
  end
26
- end
26
+ end
@@ -95,6 +95,12 @@ describe Virtus::Attribute::Integer, '#coerce' do
95
95
  it { should eql(-24) }
96
96
  end
97
97
 
98
+ context 'with a time object' do
99
+ let(:value) { Time.now }
100
+
101
+ it { should eql(value.to_i) }
102
+ end
103
+
98
104
  [ Object.new, true, false, '00.0', '0.', '-.0', 'string' ].each do |non_num_value|
99
105
  context 'does not coerce non-numeric value #{non_num_value.inspect}' do
100
106
  let(:value) { non_num_value }
@@ -1,17 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Virtus::Attribute::Object, '.descendants' do
4
- subject { described_class.descendants }
4
+ subject { described_class.descendants.map { |c| c.to_s }.sort }
5
5
 
6
6
  let(:known_descendants) do
7
- [ Virtus::Attribute::EmbeddedValue,
8
- Virtus::Attribute::Time, Virtus::Attribute::String,
9
- Virtus::Attribute::Integer, Virtus::Attribute::Hash,
10
- Virtus::Attribute::Float, Virtus::Attribute::Decimal,
11
- Virtus::Attribute::Numeric, Virtus::Attribute::DateTime,
12
- Virtus::Attribute::Date, Virtus::Attribute::Boolean,
13
- Virtus::Attribute::Set, Virtus::Attribute::Array,
14
- Virtus::Attribute::Collection, Virtus::Attribute::Class ]
7
+ [ Virtus::Attribute::EmbeddedValue, Virtus::Attribute::Symbol,
8
+ Virtus::Attribute::Time, Virtus::Attribute::String,
9
+ Virtus::Attribute::Integer, Virtus::Attribute::Hash,
10
+ Virtus::Attribute::Float, Virtus::Attribute::Decimal,
11
+ Virtus::Attribute::Numeric, Virtus::Attribute::DateTime,
12
+ Virtus::Attribute::Date, Virtus::Attribute::Boolean,
13
+ Virtus::Attribute::Set, Virtus::Attribute::Array,
14
+ Virtus::Attribute::Collection, Virtus::Attribute::Class ].map { |a| a.to_s }.sort
15
15
  end
16
16
 
17
17
  it 'should return all known attribute classes' do
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::Symbol, '#coerce' do
4
+ subject { attribute.coerce(value) }
5
+
6
+ let(:attribute) { described_class.new(:code) }
7
+
8
+ context 'with a string' do
9
+ let(:value) { 'foo' }
10
+
11
+ it { should be(:foo) }
12
+ end
13
+ end
@@ -6,4 +6,4 @@ describe Virtus::AttributesAccessor, '#inspect' do
6
6
  let(:object) { described_class.new('Test') }
7
7
 
8
8
  it { should eql('Test::AttributesAccessor') }
9
- end
9
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject, '.equalizer' do
4
+ subject { described_class.equalizer }
5
+
6
+ let(:described_class) do
7
+ Class.new do
8
+ include Virtus::ValueObject
9
+
10
+ attribute :first_name, String
11
+ end
12
+ end
13
+
14
+ specify { subject.should be_instance_of(Virtus::ValueObject::Equalizer) }
15
+ specify { described_class.included_modules.should include(subject) }
16
+
17
+ context 'when equalizer is already initialized' do
18
+ before { subject; described_class.equalizer }
19
+
20
+ let(:equalizer) { subject }
21
+
22
+ specify { subject.should be(equalizer) }
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject, '#initialize' do
4
+ subject { described_class.new(attributes) }
5
+
6
+ let(:described_class) do
7
+ Class.new do
8
+ include Virtus::ValueObject
9
+
10
+ attribute :currency, String
11
+ attribute :amount, Integer
12
+ end
13
+ end
14
+
15
+ let(:attributes) { Hash[:currency => 'USD', :amount => 1] }
16
+
17
+ its(:currency) { should eql('USD') }
18
+ its(:amount) { should eql(1) }
19
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject, '#with' do
4
+ subject { object.with(attributes) }
5
+
6
+ let(:described_class) do
7
+ Class.new do
8
+ include Virtus::ValueObject
9
+
10
+ attribute :first_name, String
11
+ attribute :last_name, String
12
+ end
13
+ end
14
+
15
+ let(:object) { described_class.new }
16
+ let(:attributes) { Hash[:first_name => 'John', :last_name => 'Doe'] }
17
+
18
+ let(:described_class) do
19
+ Class.new do
20
+ include Virtus::ValueObject
21
+
22
+ attribute :first_name, String
23
+ attribute :last_name, String
24
+ end
25
+ end
26
+
27
+ it { should be_instance_of(described_class) }
28
+
29
+ its(:first_name) { should eql('John') }
30
+ its(:last_name) { should eql('Doe') }
31
+ end
@@ -5,7 +5,6 @@ require File.expand_path('../lib/virtus/version', __FILE__)
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "virtus"
7
7
  gem.version = Virtus::VERSION
8
- gem.date = "2011-11-21"
9
8
  gem.authors = [ "Piotr Solnica" ]
10
9
  gem.email = [ "piotr.solnica@gmail.com" ]
11
10
  gem.description = "Attributes on Steroids for Plain Old Ruby Objects"
metadata CHANGED
@@ -1,59 +1,82 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: virtus
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.0
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - Piotr Solnica
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2011-11-21 00:00:00.000000000Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
17
+
18
+ date: 2012-02-08 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
15
21
  name: rake
16
- requirement: &70295232216940 !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
17
24
  none: false
18
- requirements:
25
+ requirements:
19
26
  - - ~>
20
- - !ruby/object:Gem::Version
27
+ - !ruby/object:Gem::Version
28
+ hash: 63
29
+ segments:
30
+ - 0
31
+ - 9
32
+ - 2
21
33
  version: 0.9.2
22
34
  type: :development
23
- prerelease: false
24
- version_requirements: *70295232216940
25
- - !ruby/object:Gem::Dependency
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
26
37
  name: backports
27
- requirement: &70295232216140 !ruby/object:Gem::Requirement
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
28
40
  none: false
29
- requirements:
41
+ requirements:
30
42
  - - ~>
31
- - !ruby/object:Gem::Version
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 2
47
+ - 3
48
+ - 0
32
49
  version: 2.3.0
33
50
  type: :development
34
- prerelease: false
35
- version_requirements: *70295232216140
36
- - !ruby/object:Gem::Dependency
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
37
53
  name: rspec
38
- requirement: &70295232215500 !ruby/object:Gem::Requirement
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
39
56
  none: false
40
- requirements:
57
+ requirements:
41
58
  - - ~>
42
- - !ruby/object:Gem::Version
59
+ - !ruby/object:Gem::Version
60
+ hash: 31
61
+ segments:
62
+ - 1
63
+ - 3
64
+ - 2
43
65
  version: 1.3.2
44
66
  type: :development
45
- prerelease: false
46
- version_requirements: *70295232215500
67
+ version_requirements: *id003
47
68
  description: Attributes on Steroids for Plain Old Ruby Objects
48
- email:
69
+ email:
49
70
  - piotr.solnica@gmail.com
50
71
  executables: []
72
+
51
73
  extensions: []
52
- extra_rdoc_files:
74
+
75
+ extra_rdoc_files:
53
76
  - LICENSE
54
77
  - README.md
55
78
  - TODO
56
- files:
79
+ files:
57
80
  - .gitignore
58
81
  - .rvmrc
59
82
  - .travis.yml
@@ -87,6 +110,7 @@ files:
87
110
  - lib/virtus/attribute/object.rb
88
111
  - lib/virtus/attribute/set.rb
89
112
  - lib/virtus/attribute/string.rb
113
+ - lib/virtus/attribute/symbol.rb
90
114
  - lib/virtus/attribute/time.rb
91
115
  - lib/virtus/attribute_set.rb
92
116
  - lib/virtus/attributes_accessor.rb
@@ -111,6 +135,8 @@ files:
111
135
  - lib/virtus/support/descendants_tracker.rb
112
136
  - lib/virtus/support/options.rb
113
137
  - lib/virtus/support/type_lookup.rb
138
+ - lib/virtus/value_object.rb
139
+ - lib/virtus/value_object/equalizer.rb
114
140
  - lib/virtus/version.rb
115
141
  - spec/integration/collection_member_coercion_spec.rb
116
142
  - spec/integration/custom_attributes_spec.rb
@@ -119,6 +145,7 @@ files:
119
145
  - spec/integration/embedded_value_spec.rb
120
146
  - spec/integration/overriding_virtus_spec.rb
121
147
  - spec/integration/virtus/instance_level_attributes_spec.rb
148
+ - spec/integration/virtus/value_object_spec.rb
122
149
  - spec/rcov.opts
123
150
  - spec/shared/constants_helpers.rb
124
151
  - spec/shared/idempotent_method_behaviour.rb
@@ -175,6 +202,7 @@ files:
175
202
  - spec/unit/virtus/attribute/set/coerce_spec.rb
176
203
  - spec/unit/virtus/attribute/set_spec.rb
177
204
  - spec/unit/virtus/attribute/string/coerce_spec.rb
205
+ - spec/unit/virtus/attribute/symbol/coerce_spec.rb
178
206
  - spec/unit/virtus/attribute/time/coerce_spec.rb
179
207
  - spec/unit/virtus/attribute/value_coerced_spec.rb
180
208
  - spec/unit/virtus/attribute/writer_visibility_spec.rb
@@ -238,6 +266,9 @@ files:
238
266
  - spec/unit/virtus/options/options_spec.rb
239
267
  - spec/unit/virtus/type_lookup/determine_type_spec.rb
240
268
  - spec/unit/virtus/type_lookup/primitive_spec.rb
269
+ - spec/unit/virtus/value_object/class_methods/equalizer_spec.rb
270
+ - spec/unit/virtus/value_object/initialize_spec.rb
271
+ - spec/unit/virtus/value_object/with_spec.rb
241
272
  - tasks/metrics/ci.rake
242
273
  - tasks/metrics/flay.rake
243
274
  - tasks/metrics/flog.rake
@@ -251,27 +282,36 @@ files:
251
282
  - virtus.gemspec
252
283
  homepage: https://github.com/solnic/virtus
253
284
  licenses: []
285
+
254
286
  post_install_message:
255
287
  rdoc_options: []
256
- require_paths:
288
+
289
+ require_paths:
257
290
  - lib
258
- required_ruby_version: !ruby/object:Gem::Requirement
291
+ required_ruby_version: !ruby/object:Gem::Requirement
259
292
  none: false
260
- requirements:
261
- - - ! '>='
262
- - !ruby/object:Gem::Version
263
- version: '0'
264
- required_rubygems_version: !ruby/object:Gem::Requirement
293
+ requirements:
294
+ - - ">="
295
+ - !ruby/object:Gem::Version
296
+ hash: 3
297
+ segments:
298
+ - 0
299
+ version: "0"
300
+ required_rubygems_version: !ruby/object:Gem::Requirement
265
301
  none: false
266
- requirements:
267
- - - ! '>='
268
- - !ruby/object:Gem::Version
269
- version: '0'
302
+ requirements:
303
+ - - ">="
304
+ - !ruby/object:Gem::Version
305
+ hash: 3
306
+ segments:
307
+ - 0
308
+ version: "0"
270
309
  requirements: []
310
+
271
311
  rubyforge_project:
272
- rubygems_version: 1.8.11
312
+ rubygems_version: 1.8.15
273
313
  signing_key:
274
314
  specification_version: 3
275
315
  summary: Attributes on Steroids for Plain Old Ruby Objects
276
316
  test_files: []
277
- has_rdoc:
317
+