virtus 0.1.0 → 0.2.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.
@@ -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
+