virtus 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +1 -0
  2. data/Changelog.md +11 -1
  3. data/README.md +47 -8
  4. data/config/flay.yml +1 -1
  5. data/lib/virtus.rb +25 -5
  6. data/lib/virtus/attribute.rb +1 -5
  7. data/lib/virtus/attribute/embedded_value.rb +20 -8
  8. data/lib/virtus/attribute/embedded_value/from_open_struct.rb +17 -0
  9. data/lib/virtus/attribute/embedded_value/from_struct.rb +17 -0
  10. data/lib/virtus/class_inclusions.rb +41 -0
  11. data/lib/virtus/class_methods.rb +18 -60
  12. data/lib/virtus/extensions.rb +93 -0
  13. data/lib/virtus/instance_methods.rb +7 -2
  14. data/lib/virtus/module_extensions.rb +62 -0
  15. data/lib/virtus/value_object.rb +19 -1
  16. data/lib/virtus/version.rb +1 -1
  17. data/spec/integration/default_values_spec.rb +16 -0
  18. data/spec/integration/extending_objects_spec.rb +35 -0
  19. data/spec/integration/struct_as_embedded_value_spec.rb +28 -0
  20. data/spec/integration/using_modules_spec.rb +49 -0
  21. data/spec/integration/value_object_with_custom_constructor_spec.rb +42 -0
  22. data/spec/integration/virtus/value_object_spec.rb +6 -2
  23. data/spec/unit/virtus/attribute/class_methods/determine_type_spec.rb +3 -1
  24. data/spec/unit/virtus/attribute/embedded_value/class_methods/determine_type_spec.rb +23 -0
  25. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +8 -37
  26. data/spec/unit/virtus/attribute/embedded_value/from_open_struct/coerce_spec.rb +28 -0
  27. data/spec/unit/virtus/attribute/embedded_value/from_struct/coerce_spec.rb +28 -0
  28. data/spec/unit/virtus/attribute/object/class_methods/descendants_spec.rb +2 -0
  29. data/spec/unit/virtus/class_methods/{attributes_spec.rb → attribute_set_spec.rb} +2 -2
  30. data/spec/unit/virtus/class_methods/attribute_spec.rb +1 -1
  31. data/spec/unit/virtus/{class_methods → extensions}/allowed_writer_methods_spec.rb +1 -1
  32. data/spec/unit/virtus/value_object/class_methods/attribute_spec.rb +1 -1
  33. data/spec/unit/virtus/value_object/instance_methods/duplicates_spec.rb +22 -0
  34. data/virtus.gemspec +2 -2
  35. metadata +26 -13
data/.gitignore CHANGED
@@ -33,5 +33,6 @@ measurements
33
33
  ## BUNDLER
34
34
  .bundle
35
35
  Gemfile.lock
36
+ bin/
36
37
 
37
38
  ## PROJECT::SPECIFIC
data/Changelog.md CHANGED
@@ -1,4 +1,14 @@
1
- # v0.4.1 2012-05-08
1
+ # v0.5.0 2012-06-08
2
+
3
+ * [feature] Support for extending objects (solnic)
4
+ * [feature] Support for defining attributes in modules (solnic)
5
+ * [feature] Support for Struct as an EmbeddedValue or ValueObject attribute (solnic)
6
+ * [changed] Allow any input for EmbeddedValue and ValueObject constructors (solnic)
7
+ * [changed] ValueObject instances cannot be duped or cloned (senny)
8
+
9
+ [Compare v0.4.2..master](https://github.com/solnic/virtus/compare/v0.4.2...master)
10
+
11
+ # v0.4.2 2012-05-08
2
12
 
3
13
  * [updated] Bump backports dep to ~> 2.5.3 (solnic)
4
14
 
data/README.md CHANGED
@@ -41,7 +41,7 @@ class User
41
41
  end
42
42
 
43
43
  user = User.new(:name => 'Piotr', :age => 28)
44
- user.attributes # => { :name => "Piotr", :age => 28 }
44
+ user.attribute_set # => { :name => "Piotr", :age => 28 }
45
45
 
46
46
  user.name # => "Piotr"
47
47
 
@@ -51,8 +51,48 @@ user.age.class # => Fixnum
51
51
  user.birthday = 'November 18th, 1983' # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>
52
52
  ```
53
53
 
54
+ ### Using Virtus With Modules
54
55
 
55
- **Default values**
56
+ You can create modules extended with virtus and define attributes for later
57
+ inclusion in your classes:
58
+
59
+ ```ruby
60
+ module Name
61
+ include Virtus
62
+
63
+ attribute :name, String
64
+ end
65
+
66
+ module Age
67
+ include Virtus
68
+
69
+ attribute :age, Integer
70
+ end
71
+
72
+ class User
73
+ include Name, Age
74
+ end
75
+
76
+ user = User.new(:name => 'John', :age => '30')
77
+ ```
78
+
79
+ ### Dynamically Extending Instances
80
+
81
+ It's also possible to dynamically extend an object with Virtus:
82
+
83
+ ```ruby
84
+ class User
85
+ # nothing here
86
+ end
87
+
88
+ user = User.new
89
+ user.extend(Virtus)
90
+ user.attribute :name, String
91
+ user.name = 'John'
92
+ user.name # => 'John'
93
+ ```
94
+
95
+ ### Default Values
56
96
 
57
97
  ``` ruby
58
98
  class Page
@@ -84,7 +124,7 @@ page.published # => false
84
124
  page.editor_title # => "UNPUBLISHED: Virtus README"
85
125
  ```
86
126
 
87
- **Embedded Value**
127
+ ### Embedded Value
88
128
 
89
129
  ``` ruby
90
130
  class City
@@ -115,7 +155,7 @@ user.address.street # => "Street 1/2"
115
155
  user.address.city.name # => "NYC"
116
156
  ```
117
157
 
118
- **Collection Member Coercions**
158
+ ### Collection Member Coercions
119
159
 
120
160
  ``` ruby
121
161
  # Support "primitive" classes
@@ -163,7 +203,7 @@ user.phone_numbers # => [#<PhoneNumber:0x007fdb2d3bef88 @number="212-555-1212">,
163
203
  user.addresses # => #<Set: {#<Address:0x007fdb2d3be448 @address="1234 Any St.", @locality="Anytown", @region="DC", @postal_code="21234">}>
164
204
  ```
165
205
 
166
- **Value Objects**
206
+ ### Value Objects
167
207
 
168
208
  ``` ruby
169
209
  class GeoLocation
@@ -196,7 +236,7 @@ venue_other = Venue.new(
196
236
  venue.location === venue_other.location # => true
197
237
  ```
198
238
 
199
- **Adding Coercions**
239
+ ### Adding Coercions
200
240
 
201
241
  Virtus comes with a builtin coercion library.
202
242
  It's super easy to add your own coercion classes.
@@ -235,7 +275,7 @@ user.name # => 'Piotr'
235
275
  user.password # => '3858f62230ac3c915f300c664312c63f'
236
276
  ```
237
277
 
238
- **Custom Attributes**
278
+ ### Custom Attributes
239
279
 
240
280
  ``` ruby
241
281
  require 'json'
@@ -265,7 +305,6 @@ user.info = '{"email":"john@domain.com"}' # => {"email"=>"john@domain.com"}
265
305
  user.info.class # => Hash
266
306
  ```
267
307
 
268
-
269
308
  Credits
270
309
  -------
271
310
 
data/config/flay.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  ---
2
2
  threshold: 19
3
- total_score: 365
3
+ total_score: 385
data/lib/virtus.rb CHANGED
@@ -16,21 +16,35 @@ module Virtus
16
16
  # Represents an undefined parameter used by auto-generated option methods
17
17
  Undefined = Object.new.freeze
18
18
 
19
- # Extends base class with class and instance methods
19
+ # Extends base class or a module with virtus methods
20
20
  #
21
21
  # @param [Class] descendant
22
22
  #
23
23
  # @return [undefined]
24
24
  #
25
25
  # @api private
26
- def self.included(descendant)
26
+ def self.included(object)
27
27
  super
28
- descendant.extend(ClassMethods)
29
- descendant.send(:include, InstanceMethods)
28
+ if Class === object
29
+ object.send(:include, ClassInclusions)
30
+ else
31
+ object.extend(ModuleExtensions)
32
+ end
30
33
  end
31
-
32
34
  private_class_method :included
33
35
 
36
+ # Extends an object with virtus extensions
37
+ #
38
+ # @param [Object] object
39
+ #
40
+ # @return [undefined]
41
+ #
42
+ # @api private
43
+ def self.extended(object)
44
+ object.extend(Extensions)
45
+ end
46
+ private_class_method :extended
47
+
34
48
  end # module Virtus
35
49
 
36
50
  require 'virtus/support/descendants_tracker'
@@ -38,6 +52,10 @@ require 'virtus/support/type_lookup'
38
52
  require 'virtus/support/options'
39
53
  require 'virtus/support/equalizer'
40
54
 
55
+ require 'virtus/extensions'
56
+ require 'virtus/class_inclusions'
57
+ require 'virtus/module_extensions'
58
+
41
59
  require 'virtus/attributes_accessor'
42
60
  require 'virtus/class_methods'
43
61
  require 'virtus/instance_methods'
@@ -86,3 +104,5 @@ require 'virtus/attribute/symbol'
86
104
  require 'virtus/attribute/string'
87
105
  require 'virtus/attribute/time'
88
106
  require 'virtus/attribute/embedded_value'
107
+ require 'virtus/attribute/embedded_value/from_struct'
108
+ require 'virtus/attribute/embedded_value/from_open_struct'
@@ -81,11 +81,7 @@ module Virtus
81
81
  def self.determine_type(class_or_name)
82
82
  case class_or_name
83
83
  when ::Class
84
- if class_or_name <= Virtus
85
- Attribute::EmbeddedValue
86
- else
87
- super
88
- end
84
+ Attribute::EmbeddedValue.determine_type(class_or_name) || super
89
85
  when ::Array, ::Set
90
86
  super(class_or_name.class)
91
87
  else
@@ -35,6 +35,24 @@ module Virtus
35
35
  options.merge(:primitive => type)
36
36
  end
37
37
 
38
+ # Determine type based on class
39
+ #
40
+ # Virtus::EmbeddedValue.determine_type(Struct) # => Virtus::EmbeddedValue::FromStruct
41
+ # Virtus::EmbeddedValue.determine_type(VirtusClass) # => Virtus::EmbeddedValue::FromOpenStruct
42
+ #
43
+ # @param [Class] klass
44
+ #
45
+ # @return [Virtus::Attribute::EmbeddedValue]
46
+ #
47
+ # @api public
48
+ def self.determine_type(klass)
49
+ if klass <= Virtus || klass <= OpenStruct
50
+ FromOpenStruct
51
+ elsif klass <= Struct
52
+ FromStruct
53
+ end
54
+ end
55
+
38
56
  # Coerce attributes into a virtus object
39
57
  #
40
58
  # @param [Hash,Virtus]
@@ -42,14 +60,8 @@ module Virtus
42
60
  # @return [Virtus]
43
61
  #
44
62
  # @api private
45
- def coerce(attributes_or_object)
46
- value = if attributes_or_object.kind_of?(::Hash)
47
- @primitive.new(attributes_or_object)
48
- else
49
- attributes_or_object
50
- end
51
-
52
- super(value)
63
+ def coerce(value)
64
+ value if value.kind_of?(@primitive)
53
65
  end
54
66
 
55
67
  end # class EmbeddedValue
@@ -0,0 +1,17 @@
1
+ module Virtus
2
+ class Attribute
3
+ class EmbeddedValue < Object
4
+
5
+ # EmbeddedValue attribute handling OpenStruct primitive or Virtus object
6
+ #
7
+ class FromOpenStruct < EmbeddedValue
8
+
9
+ # @api private
10
+ def coerce(attributes)
11
+ super or @primitive.new(attributes)
12
+ end
13
+
14
+ end # class FromOpenStruct
15
+ end # class EmbeddedValue
16
+ end # class Attribute
17
+ end # module Virtus
@@ -0,0 +1,17 @@
1
+ module Virtus
2
+ class Attribute
3
+ class EmbeddedValue < Object
4
+
5
+ # EmbeddedValue attribute handling Struct primitive
6
+ #
7
+ class FromStruct < EmbeddedValue
8
+
9
+ # @api private
10
+ def coerce(attributes)
11
+ super or @primitive.new(*attributes)
12
+ end
13
+
14
+ end # class FromStruct
15
+ end # class EmbeddedValue
16
+ end # class Attribute
17
+ end # module Virtus
@@ -0,0 +1,41 @@
1
+ module Virtus
2
+
3
+ # Class-level extensions
4
+ module ClassInclusions
5
+
6
+ # Extends a descendant with class and instance methods
7
+ #
8
+ # @param [Class] descendant
9
+ #
10
+ # @return [undefined]
11
+ #
12
+ # @api private
13
+ def self.included(descendant)
14
+ super
15
+ descendant.extend(ClassMethods)
16
+ descendant.send(:include, InstanceMethods)
17
+ end
18
+ private_class_method :included
19
+
20
+ private
21
+
22
+ # Return class' attribute set
23
+ #
24
+ # @return [Virtus::AttributeSet]
25
+ #
26
+ # @api private
27
+ def attribute_set
28
+ self.class.attribute_set
29
+ end
30
+
31
+ # Return a list of allowed writer method names
32
+ #
33
+ # @return [Set]
34
+ #
35
+ # @api private
36
+ def allowed_writer_methods
37
+ self.class.allowed_writer_methods
38
+ end
39
+
40
+ end # module ClassInclusions
41
+ end # module Virtus
@@ -2,8 +2,7 @@ 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
+ include Extensions
7
6
 
8
7
  # Hook called when module is extended
9
8
  #
@@ -22,39 +21,6 @@ module Virtus
22
21
 
23
22
  private_class_method :extended
24
23
 
25
- # Defines an attribute on an object's class
26
- #
27
- # @example
28
- # class Book
29
- # include Virtus
30
- #
31
- # attribute :title, String
32
- # attribute :author, String
33
- # attribute :published_at, DateTime
34
- # attribute :page_count, Integer
35
- # end
36
- #
37
- # @param [Symbol] name
38
- # the name of an attribute
39
- #
40
- # @param [Class] type
41
- # the type class of an attribute
42
- #
43
- # @param [#to_hash] options
44
- # the extra options hash
45
- #
46
- # @return [self]
47
- #
48
- # @see Attribute.build
49
- #
50
- # @api public
51
- def attribute(*args)
52
- attribute = Attribute.build(*args)
53
- attribute.define_accessor_methods(virtus_attributes_accessor_module)
54
- virtus_add_attribute(attribute)
55
- self
56
- end
57
-
58
24
  # Returns all the attributes defined on a Class
59
25
  #
60
26
  # @example
@@ -72,27 +38,22 @@ module Virtus
72
38
  # @return [AttributeSet]
73
39
  #
74
40
  # @api public
75
- def attributes
76
- return @attributes if defined?(@attributes)
41
+ def attribute_set
42
+ return @attribute_set if defined?(@attribute_set)
77
43
  superclass = self.superclass
78
44
  method = __method__
79
45
  parent = superclass.public_send(method) if superclass.respond_to?(method)
80
- @attributes = AttributeSet.new(parent)
46
+ @attribute_set = AttributeSet.new(parent)
81
47
  end
82
48
 
83
- # The list of writer methods that can be mass-assigned to in #attributes=
49
+ # @see Virtus::ClassMethods.attribute_set
84
50
  #
85
- # @return [Set]
51
+ # @deprecated
86
52
  #
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
53
+ # @api public
54
+ def attributes
55
+ warn "#{self}.attributes is deprecated. Use #{self}.attribute_set instead: #{caller.first}"
56
+ attribute_set
96
57
  end
97
58
 
98
59
  protected
@@ -104,8 +65,7 @@ module Virtus
104
65
  # @api private
105
66
  def virtus_setup_attributes_accessor_module
106
67
  @virtus_attributes_accessor_module = AttributesAccessor.new(inspect)
107
- include virtus_attributes_accessor_module
108
- self
68
+ include @virtus_attributes_accessor_module
109
69
  end
110
70
 
111
71
  private
@@ -128,13 +88,6 @@ module Virtus
128
88
  descendant.virtus_setup_attributes_accessor_module
129
89
  end
130
90
 
131
- # Holds the anonymous module which hosts this class's Attribute accessors
132
- #
133
- # @return [Module]
134
- #
135
- # @api private
136
- attr_reader :virtus_attributes_accessor_module
137
-
138
91
  # Hooks into const missing process to determine types of attributes
139
92
  #
140
93
  # @param [String] name
@@ -154,8 +107,13 @@ module Virtus
154
107
  #
155
108
  # @api private
156
109
  def virtus_add_attribute(attribute)
157
- attributes << attribute
158
- descendants.each { |descendant| descendant.attributes.reset }
110
+ super
111
+ descendants.each { |descendant| descendant.attribute_set.reset }
112
+ end
113
+
114
+ # @api private
115
+ def public_method_list
116
+ public_instance_methods
159
117
  end
160
118
 
161
119
  end # module ClassMethods
@@ -0,0 +1,93 @@
1
+ module Virtus
2
+
3
+ # Extensions common for both classes and instances
4
+ module Extensions
5
+ WRITER_METHOD_REGEXP = /=\z/.freeze
6
+ INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze
7
+
8
+ # A hook called when an object is extended with Virtus
9
+ #
10
+ # @param [Object] object
11
+ #
12
+ # @return [undefined]
13
+ #
14
+ # @api private
15
+ def self.extended(object)
16
+ object.extend(InstanceMethods)
17
+ object.instance_eval do
18
+ @virtus_attributes_accessor_module = AttributesAccessor.new(object.class.inspect)
19
+ extend @virtus_attributes_accessor_module
20
+ end
21
+ end
22
+ private_class_method :extended
23
+
24
+ # Defines an attribute on an object's class
25
+ #
26
+ # @example
27
+ # class Book
28
+ # include Virtus
29
+ #
30
+ # attribute :title, String
31
+ # attribute :author, String
32
+ # attribute :published_at, DateTime
33
+ # attribute :page_count, Integer
34
+ # end
35
+ #
36
+ # @param [Symbol] name
37
+ # the name of an attribute
38
+ #
39
+ # @param [Class] type
40
+ # the type class of an attribute
41
+ #
42
+ # @param [#to_hash] options
43
+ # the extra options hash
44
+ #
45
+ # @return [self]
46
+ #
47
+ # @see Attribute.build
48
+ #
49
+ # @api public
50
+ def attribute(*args)
51
+ attribute = Attribute.build(*args)
52
+ attribute.define_accessor_methods(@virtus_attributes_accessor_module)
53
+ virtus_add_attribute(attribute)
54
+ self
55
+ end
56
+
57
+ # The list of writer methods that can be mass-assigned to in #attributes=
58
+ #
59
+ # @return [Set]
60
+ #
61
+ # @api private
62
+ def allowed_writer_methods
63
+ @allowed_writer_methods ||=
64
+ begin
65
+ allowed_writer_methods = public_method_list.map(&:to_s)
66
+ allowed_writer_methods = allowed_writer_methods.grep(WRITER_METHOD_REGEXP).to_set
67
+ allowed_writer_methods -= INVALID_WRITER_METHODS
68
+ allowed_writer_methods.freeze
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ # Return an attribute set for that instance
75
+ #
76
+ # @return [AttributeSet]
77
+ #
78
+ # @api private
79
+ def attribute_set
80
+ @attribute_set ||= AttributeSet.new
81
+ end
82
+
83
+ # Add an attribute to the attribute set
84
+ #
85
+ # @return [AttributeSet]
86
+ #
87
+ # @api private
88
+ def virtus_add_attribute(attribute)
89
+ attribute_set << attribute
90
+ end
91
+
92
+ end # module Extensions
93
+ end # module Virtus
@@ -140,7 +140,7 @@ module Virtus
140
140
  #
141
141
  # @api private
142
142
  def get_attributes
143
- self.class.attributes.each_with_object({}) do |attribute, attributes|
143
+ attribute_set.each_with_object({}) do |attribute, attributes|
144
144
  name = attribute.name
145
145
  attributes[name] = get_attribute(name) if yield(attribute)
146
146
  end
@@ -155,7 +155,7 @@ module Virtus
155
155
  # @api private
156
156
  def set_attributes(attributes)
157
157
  ::Hash.try_convert(attributes).each do |name, value|
158
- set_attribute(name, value) if self.class.allowed_writer_methods.include?("#{name}=")
158
+ set_attribute(name, value) if allowed_writer_methods.include?("#{name}=")
159
159
  end
160
160
  end
161
161
 
@@ -181,5 +181,10 @@ module Virtus
181
181
  __send__("#{name}=", value)
182
182
  end
183
183
 
184
+ # @api private
185
+ def public_method_list
186
+ public_methods
187
+ end
188
+
184
189
  end # module InstanceMethods
185
190
  end # module Virtus
@@ -0,0 +1,62 @@
1
+ module Virtus
2
+
3
+ # Virtus module that can define attributes for later inclusion
4
+ #
5
+ module ModuleExtensions
6
+
7
+ def attribute(*args)
8
+ attribute_definitions << args
9
+ end
10
+
11
+ private
12
+
13
+ # Extend an object with Virtus methods and define attributes
14
+ #
15
+ # @param [Object] object
16
+ #
17
+ # @return [undefined]
18
+ #
19
+ # @api private
20
+ def extended(object)
21
+ super
22
+ object.extend(Virtus)
23
+ define_attributes(object)
24
+ end
25
+
26
+ # Extend a class with Virtus methods and define attributes
27
+ #
28
+ # @param [Object] object
29
+ #
30
+ # @return [undefined]
31
+ #
32
+ # @api private
33
+ def included(object)
34
+ super
35
+ object.send(:include, ClassInclusions)
36
+ define_attributes(object)
37
+ end
38
+
39
+ # Return attribute definitions
40
+ #
41
+ # @return [Array<Hash>]
42
+ #
43
+ # @api private
44
+ def attribute_definitions
45
+ @_attribute_definitions ||= []
46
+ end
47
+
48
+ # Define attributes on a class or instance
49
+ #
50
+ # @param [Object,Class] object
51
+ #
52
+ # @return [undefined]
53
+ #
54
+ # @api private
55
+ def define_attributes(object)
56
+ attribute_definitions.each do |attribute_args|
57
+ object.attribute(*attribute_args)
58
+ end
59
+ end
60
+
61
+ end # class ModuleExtensions
62
+ end # module Virtus
@@ -41,6 +41,7 @@ module Virtus
41
41
  private_class_method :included
42
42
 
43
43
  module InstanceMethods
44
+
44
45
  # the #get_attributes method accept a Proc object that will filter
45
46
  # out an attribute when the block returns false. the ValueObject
46
47
  # needs all the attributes, so we allow every attribute.
@@ -53,6 +54,23 @@ module Virtus
53
54
  def with(attribute_updates)
54
55
  self.class.new(get_attributes(&FILTER_NONE).merge(attribute_updates))
55
56
  end
57
+
58
+ # ValueObjects are immutable and can't be cloned
59
+ #
60
+ # They always represent the same value
61
+ #
62
+ # @example
63
+ #
64
+ # value_object.clone === value_object # => true
65
+ #
66
+ # @return [self]
67
+ #
68
+ # @api public
69
+ def clone
70
+ self
71
+ end
72
+ alias dup clone
73
+
56
74
  end
57
75
 
58
76
  module ClassMethods
@@ -112,7 +130,7 @@ module Virtus
112
130
  @allowed_writer_methods ||=
113
131
  begin
114
132
  allowed_writer_methods = super
115
- allowed_writer_methods += attributes.map{|attr| "#{attr.name}="}
133
+ allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="}
116
134
  allowed_writer_methods.to_set.freeze
117
135
  end
118
136
  end
@@ -1,3 +1,3 @@
1
1
  module Virtus
2
- VERSION = '0.4.2'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -4,6 +4,13 @@ describe "default values" do
4
4
 
5
5
  before do
6
6
  module Examples
7
+
8
+ class Reference
9
+ include Virtus::ValueObject
10
+
11
+ attribute :ref, String
12
+ end
13
+
7
14
  class Page
8
15
  include Virtus
9
16
 
@@ -12,6 +19,7 @@ describe "default values" do
12
19
  attribute :view_count, Integer, :default => 0
13
20
  attribute :published, Boolean, :default => false, :accessor => :private
14
21
  attribute :editor_title, String, :default => :default_editor_title
22
+ attribute :reference, String, :default => Reference.new
15
23
 
16
24
  def default_editor_title
17
25
  published? ? title : "UNPUBLISHED: #{title}"
@@ -41,4 +49,12 @@ describe "default values" do
41
49
  subject.editor_title.should == 'UNPUBLISHED: Top Secret'
42
50
  end
43
51
 
52
+ context 'with a ValueObject' do
53
+ it 'should not duplicate the ValueObject' do
54
+ page1 = Examples::Page.new
55
+ page2 = Examples::Page.new
56
+ page1.reference.should equal(page2.reference)
57
+ end
58
+ end
59
+
44
60
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'I can extend objects' do
4
+ before do
5
+ module Examples
6
+ class User; end
7
+
8
+ class Admin; end
9
+ end
10
+ end
11
+
12
+ specify 'defining attributes on an object' do
13
+ attributes = { :name => 'John', :age => 29 }
14
+
15
+ admin = Examples::Admin.new
16
+ admin.extend(Virtus)
17
+
18
+ admin.attribute :name, String
19
+ admin.attribute :age, Integer
20
+
21
+ admin.name = 'John'
22
+ admin.age = 29
23
+
24
+ admin.name.should eql('John')
25
+ admin.age.should eql(29)
26
+
27
+ admin.attributes.should eql(attributes)
28
+
29
+ new_attributes = { :name => 'Jane', :age => 28 }
30
+ admin.attributes = new_attributes
31
+
32
+ admin.name.should eql('Jane')
33
+ admin.age.should eql(28)
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Using Struct as an embedded value attribute' do
4
+ before do
5
+ module Examples
6
+ Point = Struct.new(:x, :y)
7
+
8
+ class Rectangle
9
+ include Virtus
10
+
11
+ attribute :top_left, Point
12
+ attribute :bottom_right, Point
13
+ end
14
+ end
15
+ end
16
+
17
+ subject do
18
+ Examples::Rectangle.new(:top_left => [ 3, 5 ], :bottom_right => [ 8, 7 ])
19
+ end
20
+
21
+ specify 'initialize a struct object with correct attributes' do
22
+ subject.top_left.x.should be(3)
23
+ subject.top_left.y.should be(5)
24
+
25
+ subject.bottom_right.x.should be(8)
26
+ subject.bottom_right.y.should be(7)
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'I can define attributes within a module' do
4
+ before do
5
+ module Examples
6
+ module Name
7
+ include Virtus
8
+
9
+ attribute :name, String
10
+ end
11
+
12
+ module Age
13
+ include Virtus
14
+
15
+ attribute :age, Integer
16
+ end
17
+
18
+ class User
19
+ include Name
20
+ end
21
+
22
+ class Admin < User
23
+ include Age
24
+ end
25
+
26
+ class Moderator; end
27
+ end
28
+ end
29
+
30
+ specify 'including a module with attributes into a class' do
31
+ Examples::User.attribute_set[:name].should be_instance_of(Virtus::Attribute::String)
32
+
33
+ Examples::Admin.attribute_set[:name].should be_instance_of(Virtus::Attribute::String)
34
+ Examples::Admin.attribute_set[:age].should be_instance_of(Virtus::Attribute::Integer)
35
+
36
+ user = Examples::Admin.new(:name => 'Piotr', :age => 29)
37
+ user.name.should eql('Piotr')
38
+ user.age.should eql(29)
39
+ end
40
+
41
+ specify 'including a module with attributes into an instance' do
42
+ moderator = Examples::Moderator.new
43
+ moderator.extend(Examples::Name, Examples::Age)
44
+
45
+ moderator.attributes = { :name => 'John', :age => 21 }
46
+ moderator.name.should eql('John')
47
+ moderator.age.should eql(21)
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Defining a ValueObject with a custom constructor" do
4
+ before do
5
+ module Examples
6
+ class Point
7
+ include Virtus::ValueObject
8
+
9
+ attribute :x, Integer
10
+ attribute :y, Integer
11
+
12
+ def initialize(attributes)
13
+ if attributes.kind_of?(Array)
14
+ self.x = attributes.first
15
+ self.y = attributes.last
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
21
+
22
+ class Rectangle
23
+ include Virtus
24
+
25
+ attribute :top_left, Point
26
+ attribute :bottom_right, Point
27
+ end
28
+ end
29
+ end
30
+
31
+ subject do
32
+ Examples::Rectangle.new(:top_left => [ 3, 4 ], :bottom_right => [ 5, 8 ])
33
+ end
34
+
35
+ specify "initialize a value object attribute with correct attributes" do
36
+ subject.top_left.x.should be(3)
37
+ subject.top_left.y.should be(4)
38
+
39
+ subject.bottom_right.x.should be(5)
40
+ subject.bottom_right.y.should be(8)
41
+ end
42
+ end
@@ -13,11 +13,15 @@ describe Virtus::ValueObject do
13
13
  attribute :longitude, Float
14
14
  end
15
15
  end
16
+
16
17
  let(:attribute_values) { { :latitude => 10.0, :longitude => 20.0 } }
18
+
17
19
  let(:instance_with_equal_state) { class_under_test.new(attribute_values) }
20
+
18
21
  let(:instance_with_different_state) do
19
22
  class_under_test.new(:latitude => attribute_values[:latitude])
20
23
  end
24
+
21
25
  subject { class_under_test.new(attribute_values) }
22
26
 
23
27
  describe 'initialization' do
@@ -29,8 +33,8 @@ describe Virtus::ValueObject do
29
33
 
30
34
  describe 'writer visibility' do
31
35
  it 'attributes are configured for private writers' do
32
- class_under_test.attributes[:latitude].public_reader?.should be(true)
33
- class_under_test.attributes[:longitude].public_writer?.should be(false)
36
+ class_under_test.attribute_set[:latitude].public_reader?.should be(true)
37
+ class_under_test.attribute_set[:longitude].public_writer?.should be(false)
34
38
  end
35
39
 
36
40
  it 'writer methods are set to private' do
@@ -4,6 +4,8 @@ describe Virtus::Attribute, '.determine_type' do
4
4
  let(:object) { described_class }
5
5
 
6
6
  (described_class.descendants - [ Virtus::Attribute::Collection ]).each do |attribute_class|
7
+ next if attribute_class <= Virtus::Attribute::EmbeddedValue
8
+
7
9
  context "with class #{attribute_class.inspect}" do
8
10
  subject { object.determine_type(attribute_class) }
9
11
 
@@ -36,7 +38,7 @@ describe Virtus::Attribute, '.determine_type' do
36
38
 
37
39
  let(:primitive) { Class.new { include Virtus } }
38
40
 
39
- it { should equal(Virtus::Attribute::EmbeddedValue) }
41
+ it { should equal(Virtus::Attribute::EmbeddedValue::FromOpenStruct) }
40
42
  end
41
43
 
42
44
  context 'when the primitive defaults to Object' do
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::EmbeddedValue, '.determine_type' do
4
+ subject { described_class.determine_type(value) }
5
+
6
+ context "with Struct" do
7
+ let(:value) { Struct.new(:x) }
8
+
9
+ it { should be(Virtus::Attribute::EmbeddedValue::FromStruct) }
10
+ end
11
+
12
+ context "with OpenStruct" do
13
+ let(:value) { OpenStruct }
14
+
15
+ it { should be(Virtus::Attribute::EmbeddedValue::FromOpenStruct) }
16
+ end
17
+
18
+ context "with a Virtus descendant" do
19
+ let(:value) { Class.new { include Virtus; self } }
20
+
21
+ it { should be(Virtus::Attribute::EmbeddedValue::FromOpenStruct) }
22
+ end
23
+ end
@@ -3,47 +3,18 @@ require 'spec_helper'
3
3
  describe Virtus::Attribute::EmbeddedValue, '#coerce' do
4
4
  subject { object.coerce(value) }
5
5
 
6
- let(:attribute_name) { :attribute_name }
7
- let(:model) { OpenStruct }
8
- let(:instance) { Object.new }
6
+ let(:object) { described_class.new(:name, :primitive => primitive) }
7
+ let(:instance) { Object.new }
8
+ let(:value) { primitive.new }
9
9
 
10
- context 'when the value is a hash' do
11
- let(:value) { Hash[:foo => 'bar'] }
10
+ context 'when the value is a virtus object' do
11
+ let(:primitive) { Class.new { include Virtus } }
12
12
 
13
- context 'when the options include the model' do
14
- let(:object) { described_class.new(attribute_name, :primitive => model) }
15
- let(:model) { mock('model') }
16
- let(:model_instance) { mock('model_instance') }
17
-
18
- before do
19
- model.should_receive(:new).with(value).and_return(model_instance)
20
- end
21
-
22
- it { should be(model_instance) }
23
- end
24
-
25
- context 'with the default OpenStruct model' do
26
- let(:model_instance) { OpenStruct.new(value) }
27
-
28
- context 'when the options are an empty Hash' do
29
- let(:object) { described_class.new(attribute_name, {}) }
30
-
31
- it { should == model_instance }
32
- end
33
-
34
- context 'when the options are not provided' do
35
- let(:object) { described_class.new(attribute_name) }
36
-
37
- it { should == model_instance }
38
- end
39
- end
13
+ it { should be(value) }
40
14
  end
41
15
 
42
- context 'when the value is not a hash' do
43
- let(:object) { described_class.new(attribute_name) }
44
- let(:value) { mock('value') }
45
-
46
- before { model.should_not_receive(:new) }
16
+ context 'when the value is a virtus value object' do
17
+ let(:primitive) { Class.new { include Virtus::ValueObject } }
47
18
 
48
19
  it { should be(value) }
49
20
  end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::EmbeddedValue::FromOpenStruct, '#coerce' do
4
+ subject { object.coerce(value) }
5
+
6
+ let(:primitive) { OpenStruct }
7
+
8
+ let(:object) do
9
+ described_class.new(:name, :primitive => primitive)
10
+ end
11
+
12
+ context 'when the value is a primitive instance' do
13
+ let(:value) { primitive.new }
14
+
15
+ it { should be(value) }
16
+ end
17
+
18
+ context 'when the value is a hash' do
19
+ let(:value) { Hash[:foo => 'bar'] }
20
+ let(:instance) { mock('instance') }
21
+
22
+ before do
23
+ primitive.should_receive(:new).with(value).and_return(instance)
24
+ end
25
+
26
+ it { should be(instance) }
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::Attribute::EmbeddedValue::FromStruct, '#coerce' do
4
+ subject { object.coerce(value) }
5
+
6
+ let(:primitive) { Struct.new(:x, :y) }
7
+
8
+ let(:object) do
9
+ described_class.new(:name, :primitive => primitive)
10
+ end
11
+
12
+ context 'when the value is a primitive instance' do
13
+ let(:value) { primitive.new }
14
+
15
+ it { should be(value) }
16
+ end
17
+
18
+ context 'when the value is an array' do
19
+ let(:value) { [ 1 ,2 ] }
20
+ let(:instance) { mock('instance') }
21
+
22
+ before do
23
+ primitive.should_receive(:new).with(*value).and_return(instance)
24
+ end
25
+
26
+ it { should be(instance) }
27
+ end
28
+ end
@@ -3,6 +3,8 @@ require 'spec_helper'
3
3
  describe Virtus::Attribute::Object, '.descendants' do
4
4
  subject { described_class.descendants.map { |c| c.to_s }.sort }
5
5
 
6
+ before { pending 'Remove this spec in favor of Virtus::DescentantsTracker spec' }
7
+
6
8
  let(:known_descendants) do
7
9
  [ Virtus::Attribute::EmbeddedValue, Virtus::Attribute::Symbol,
8
10
  Virtus::Attribute::Time, Virtus::Attribute::String,
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Virtus::ClassMethods, '#attributes' do
4
- subject { object.attributes }
3
+ describe Virtus::ClassMethods, '#attribute_set' do
4
+ subject { object.attribute_set }
5
5
 
6
6
  let(:object) { Class.new { extend Virtus::ClassMethods } }
7
7
 
@@ -9,7 +9,7 @@ describe Virtus::ClassMethods, '#attribute' do
9
9
  let(:type) { Virtus::Attribute::String }
10
10
 
11
11
  def assert_attribute_added(klass, name, attribute_class)
12
- attributes = klass.attributes
12
+ attributes = klass.attribute_set
13
13
  attributes[name].should be_nil
14
14
  subject
15
15
  attribute = attributes[name]
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Virtus::ClassMethods, '#allowed_writer_methods' do
3
+ describe Virtus::Extensions, '#allowed_writer_methods' do
4
4
  subject { object.allowed_writer_methods }
5
5
 
6
6
  let(:object) do
@@ -6,7 +6,7 @@ describe Virtus::ValueObject, '.attribute' do
6
6
  let(:object) { Class.new { include Virtus::ValueObject } }
7
7
  let(:name) { :latitude }
8
8
  let(:type) { Float }
9
- let(:attribute) { object.attributes[name] }
9
+ let(:attribute) { object.attribute_set[name] }
10
10
 
11
11
  it { should be(object) }
12
12
 
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Virtus::ValueObject::InstanceMethods, 'duplication' do
4
+ let(:described_class) do
5
+ Class.new do
6
+ include Virtus::ValueObject
7
+
8
+ attribute :name, String
9
+ end
10
+ end
11
+
12
+ subject { described_class.new }
13
+
14
+ it '#clone returns the same instance' do
15
+ subject.should equal(subject.clone)
16
+ end
17
+
18
+ it '#dup returns the same instance' do
19
+ subject.should equal(subject.dup)
20
+ end
21
+
22
+ end
data/virtus.gemspec CHANGED
@@ -16,9 +16,9 @@ Gem::Specification.new do |gem|
16
16
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
17
17
  gem.extra_rdoc_files = %w[LICENSE README.md TODO]
18
18
 
19
- gem.add_runtime_dependency('backports', '~> 2.5.3')
19
+ gem.add_runtime_dependency('backports', '~> 2.6.1')
20
20
 
21
21
  gem.add_development_dependency('rake', '~> 0.9.2')
22
22
  gem.add_development_dependency('rspec', '~> 1.3.2')
23
- gem.add_development_dependency('guard-rspec', '~> 0.6.0')
23
+ gem.add_development_dependency('guard-rspec', '~> 1.0.0')
24
24
  end
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
9
- - 2
10
- version: 0.4.2
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Piotr Solnica
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-05-08 00:00:00 Z
18
+ date: 2012-06-08 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: backports
@@ -25,12 +25,12 @@ dependencies:
25
25
  requirements:
26
26
  - - ~>
27
27
  - !ruby/object:Gem::Version
28
- hash: 29
28
+ hash: 21
29
29
  segments:
30
30
  - 2
31
- - 5
32
- - 3
33
- version: 2.5.3
31
+ - 6
32
+ - 1
33
+ version: 2.6.1
34
34
  type: :runtime
35
35
  version_requirements: *id001
36
36
  - !ruby/object:Gem::Dependency
@@ -73,12 +73,12 @@ dependencies:
73
73
  requirements:
74
74
  - - ~>
75
75
  - !ruby/object:Gem::Version
76
- hash: 7
76
+ hash: 23
77
77
  segments:
78
+ - 1
78
79
  - 0
79
- - 6
80
80
  - 0
81
- version: 0.6.0
81
+ version: 1.0.0
82
82
  type: :development
83
83
  version_requirements: *id004
84
84
  description: Attributes on Steroids for Plain Old Ruby Objects
@@ -124,6 +124,8 @@ files:
124
124
  - lib/virtus/attribute/default_value/from_clonable.rb
125
125
  - lib/virtus/attribute/default_value/from_symbol.rb
126
126
  - lib/virtus/attribute/embedded_value.rb
127
+ - lib/virtus/attribute/embedded_value/from_open_struct.rb
128
+ - lib/virtus/attribute/embedded_value/from_struct.rb
127
129
  - lib/virtus/attribute/float.rb
128
130
  - lib/virtus/attribute/hash.rb
129
131
  - lib/virtus/attribute/integer.rb
@@ -135,6 +137,7 @@ files:
135
137
  - lib/virtus/attribute/time.rb
136
138
  - lib/virtus/attribute_set.rb
137
139
  - lib/virtus/attributes_accessor.rb
140
+ - lib/virtus/class_inclusions.rb
138
141
  - lib/virtus/class_methods.rb
139
142
  - lib/virtus/coercion.rb
140
143
  - lib/virtus/coercion/array.rb
@@ -152,7 +155,9 @@ files:
152
155
  - lib/virtus/coercion/time.rb
153
156
  - lib/virtus/coercion/time_coercions.rb
154
157
  - lib/virtus/coercion/true_class.rb
158
+ - lib/virtus/extensions.rb
155
159
  - lib/virtus/instance_methods.rb
160
+ - lib/virtus/module_extensions.rb
156
161
  - lib/virtus/support/descendants_tracker.rb
157
162
  - lib/virtus/support/equalizer.rb
158
163
  - lib/virtus/support/options.rb
@@ -164,8 +169,12 @@ files:
164
169
  - spec/integration/default_values_spec.rb
165
170
  - spec/integration/defining_attributes_spec.rb
166
171
  - spec/integration/embedded_value_spec.rb
172
+ - spec/integration/extending_objects_spec.rb
167
173
  - spec/integration/mass_assignment_with_accessors_spec.rb
168
174
  - spec/integration/overriding_virtus_spec.rb
175
+ - spec/integration/struct_as_embedded_value_spec.rb
176
+ - spec/integration/using_modules_spec.rb
177
+ - spec/integration/value_object_with_custom_constructor_spec.rb
169
178
  - spec/integration/virtus/instance_level_attributes_spec.rb
170
179
  - spec/integration/virtus/value_object_spec.rb
171
180
  - spec/rcov.opts
@@ -214,8 +223,11 @@ files:
214
223
  - spec/unit/virtus/attribute/define_accessor_methods_spec.rb
215
224
  - spec/unit/virtus/attribute/define_reader_method_spec.rb
216
225
  - spec/unit/virtus/attribute/define_writer_method_spec.rb
226
+ - spec/unit/virtus/attribute/embedded_value/class_methods/determine_type_spec.rb
217
227
  - spec/unit/virtus/attribute/embedded_value/class_methods/merge_options_spec.rb
218
228
  - spec/unit/virtus/attribute/embedded_value/coerce_spec.rb
229
+ - spec/unit/virtus/attribute/embedded_value/from_open_struct/coerce_spec.rb
230
+ - spec/unit/virtus/attribute/embedded_value/from_struct/coerce_spec.rb
219
231
  - spec/unit/virtus/attribute/float/coerce_spec.rb
220
232
  - spec/unit/virtus/attribute/get_spec.rb
221
233
  - spec/unit/virtus/attribute/inspect_spec.rb
@@ -243,9 +255,8 @@ files:
243
255
  - spec/unit/virtus/attributes_accessor/define_reader_method_spec.rb
244
256
  - spec/unit/virtus/attributes_accessor/define_writer_method_spec.rb
245
257
  - spec/unit/virtus/attributes_accessor/inspect_spec.rb
246
- - spec/unit/virtus/class_methods/allowed_writer_methods_spec.rb
258
+ - spec/unit/virtus/class_methods/attribute_set_spec.rb
247
259
  - spec/unit/virtus/class_methods/attribute_spec.rb
248
- - spec/unit/virtus/class_methods/attributes_spec.rb
249
260
  - spec/unit/virtus/class_methods/const_missing_spec.rb
250
261
  - spec/unit/virtus/class_methods/inherited_spec.rb
251
262
  - spec/unit/virtus/coercion/array/class_methods/to_set_spec.rb
@@ -308,6 +319,7 @@ files:
308
319
  - spec/unit/virtus/equalizer/class_method/new_spec.rb
309
320
  - spec/unit/virtus/equalizer/methods/eql_spec.rb
310
321
  - spec/unit/virtus/equalizer/methods/equal_value_spec.rb
322
+ - spec/unit/virtus/extensions/allowed_writer_methods_spec.rb
311
323
  - spec/unit/virtus/instance_methods/attributes_spec.rb
312
324
  - spec/unit/virtus/instance_methods/element_reference_spec.rb
313
325
  - spec/unit/virtus/instance_methods/element_set_spec.rb
@@ -323,6 +335,7 @@ files:
323
335
  - spec/unit/virtus/value_object/class_methods/attribute_spec.rb
324
336
  - spec/unit/virtus/value_object/class_methods/equalizer_spec.rb
325
337
  - spec/unit/virtus/value_object/initialize_spec.rb
338
+ - spec/unit/virtus/value_object/instance_methods/duplicates_spec.rb
326
339
  - spec/unit/virtus/value_object/instance_methods/with_spec.rb
327
340
  - tasks/metrics/ci.rake
328
341
  - tasks/metrics/flay.rake