public
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/rails/rails.git
Added :constructor and :converter options to composed_of and deprecated the 
conversion block

Signed-off-by: Michael Koziarski <michael@koziarski.com>
Mon Aug 18 11:13:01 -0700 2008
NZKoz (committer)
Wed Sep 10 09:28:47 -0700 2008
commit  2cee51d5c1d143f6fe0096ba6cbd1db1ecbe2d90
tree    69385e859aef550418365d4553bebe5a4af5cb77
parent  7c9851dbb6f841ffbf3ebd16e9f57c04319d3a39
...
10
11
12
13
 
14
15
16
 
 
17
18
19
...
30
31
32
33
34
35
36
 
 
 
 
37
38
39
...
56
57
58
59
60
 
 
61
62
63
64
 
 
65
66
67
68
69
70
71
 
72
73
74
...
87
88
89
90
91
 
 
92
93
94
...
103
104
105
106
 
107
108
 
109
110
111
...
130
131
132
133
134
135
 
 
 
 
 
136
137
138
139
 
140
141
142
 
 
 
 
 
143
144
145
 
146
147
148
149
 
 
150
151
 
 
 
 
 
 
 
 
152
153
154
155
156
157
158
159
160
 
161
162
163
164
165
 
 
 
 
 
 
 
 
 
 
166
167
168
...
170
171
172
173
 
174
175
176
177
178
179
180
 
 
 
 
 
 
 
 
 
 
181
182
183
...
10
11
12
 
13
14
 
 
15
16
17
18
19
...
30
31
32
 
 
 
 
33
34
35
36
37
38
39
...
56
57
58
 
 
59
60
61
62
 
 
63
64
65
66
67
68
69
70
 
71
72
73
74
...
87
88
89
 
 
90
91
92
93
94
...
103
104
105
 
106
107
 
108
109
110
111
...
130
131
132
 
 
 
133
134
135
136
137
138
139
140
 
141
142
143
144
145
146
147
148
149
150
151
 
152
153
154
 
 
155
156
157
 
158
159
160
161
162
163
164
165
166
 
 
 
167
168
169
170
 
171
172
173
174
175
 
176
177
178
179
180
181
182
183
184
185
186
187
188
...
190
191
192
 
193
194
195
196
197
198
199
 
200
201
202
203
204
205
206
207
208
209
210
211
212
0
@@ -10,10 +10,10 @@ module ActiveRecord
0
       end unless self.new_record?
0
     end
0
 
0
-    # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes 
0
+    # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
0
     # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
0
-    # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the 
0
-    # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object) 
0
+    # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
0
+    # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
0
     # and how it can be turned back into attributes (when the entity is saved to the database). Example:
0
     #
0
     #   class Customer < ActiveRecord::Base
0
@@ -30,10 +30,10 @@ module ActiveRecord
0
     #  class Money
0
     #    include Comparable
0
     #    attr_reader :amount, :currency
0
-    #    EXCHANGE_RATES = { "USD_TO_DKK" => 6 }  
0
-    # 
0
-    #    def initialize(amount, currency = "USD") 
0
-    #      @amount, @currency = amount, currency 
0
+    #    EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
0
+    #
0
+    #    def initialize(amount, currency = "USD")
0
+    #      @amount, @currency = amount, currency
0
     #    end
0
     #
0
     #    def exchange_to(other_currency)
0
@@ -56,19 +56,19 @@ module ActiveRecord
0
     #
0
     #  class Address
0
     #    attr_reader :street, :city
0
-    #    def initialize(street, city) 
0
-    #      @street, @city = street, city 
0
+    #    def initialize(street, city)
0
+    #      @street, @city = street, city
0
     #    end
0
     #
0
-    #    def close_to?(other_address) 
0
-    #      city == other_address.city 
0
+    #    def close_to?(other_address)
0
+    #      city == other_address.city
0
     #    end
0
     #
0
     #    def ==(other_address)
0
     #      city == other_address.city && street == other_address.street
0
     #    end
0
     #  end
0
-    #  
0
+    #
0
     # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
0
     # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
0
     # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
0
@@ -87,8 +87,8 @@ module ActiveRecord
0
     #   customer.address_city   = "Copenhagen"
0
     #   customer.address        # => Address.new("Hyancintvej", "Copenhagen")
0
     #   customer.address = Address.new("May Street", "Chicago")
0
-    #   customer.address_street # => "May Street" 
0
-    #   customer.address_city   # => "Chicago" 
0
+    #   customer.address_street # => "May Street"
0
+    #   customer.address_city   # => "Chicago"
0
     #
0
     # == Writing value objects
0
     #
0
@@ -103,9 +103,9 @@ module ActiveRecord
0
     # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
0
     # changed through means other than the writer method.
0
     #
0
-    # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to 
0
+    # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
0
     # change it afterwards will result in a ActiveSupport::FrozenObjectError.
0
-    # 
0
+    #
0
     # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
0
     # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
0
     #
0
@@ -130,39 +130,59 @@ module ActiveRecord
0
       # * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
0
       #   attributes are +nil+.  Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
0
       #   This defaults to +false+.
0
-      #
0
-      # An optional block can be passed to convert the argument that is passed to the writer method into an instance of
0
-      # <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
0
+      # * <tt>:constructor</tt> - a symbol specifying the name of the constructor method or a Proc that will be used to convert the
0
+      #   attributes that are mapped to the aggregation to instantiate a <tt>:class_name</tt> object. The default is +:new+.
0
+      # * <tt>:converter</tt> - a symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that will be used to convert
0
+      #   the argument that is passed to the writer method into an instance of <tt>:class_name</tt>. The converter will only be called
0
+      #   if the argument is not already an instance of <tt>:class_name</tt>.
0
       #
0
       # Option examples:
0
       #   composed_of :temperature, :mapping => %w(reading celsius)
0
-      #   composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
0
+      #   composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
0
       #   composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
0
       #   composed_of :gps_location
0
       #   composed_of :gps_location, :allow_nil => true
0
+      #   composed_of :ip_address,
0
+      #               :class_name => 'IPAddr',
0
+      #               :mapping => %w(ip to_i),
0
+      #               :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
0
+      #               :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
0
       #
0
       def composed_of(part_id, options = {}, &block)
0
-        options.assert_valid_keys(:class_name, :mapping, :allow_nil)
0
+        options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
0
 
0
         name        = part_id.id2name
0
-        class_name  = options[:class_name] || name.camelize
0
-        mapping     = options[:mapping]    || [ name, name ]
0
+        class_name  = options[:class_name]  || name.camelize
0
+        mapping     = options[:mapping]     || [ name, name ]
0
         mapping     = [ mapping ] unless mapping.first.is_a?(Array)
0
-        allow_nil   = options[:allow_nil]  || false
0
+        allow_nil   = options[:allow_nil]   || false
0
+        constructor = options[:constructor] || :new
0
+        converter   = options[:converter]   || block
0
+
0
+        ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
0
+
0
+        reader_method(name, class_name, mapping, allow_nil, constructor)
0
+        writer_method(name, class_name, mapping, allow_nil, converter)
0
 
0
-        reader_method(name, class_name, mapping, allow_nil)
0
-        writer_method(name, class_name, mapping, allow_nil, block)
0
-        
0
         create_reflection(:composed_of, part_id, options, self)
0
       end
0
 
0
       private
0
-        def reader_method(name, class_name, mapping, allow_nil)
0
+        def reader_method(name, class_name, mapping, allow_nil, constructor)
0
           module_eval do
0
             define_method(name) do |*args|
0
               force_reload = args.first || false
0
               if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
0
-                instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
0
+                attrs = mapping.collect {|pair| read_attribute(pair.first)}
0
+                object = case constructor
0
+                  when Symbol
0
+                    class_name.constantize.send(constructor, *attrs)
0
+                  when Proc, Method
0
+                    constructor.call(*attrs)
0
+                  else
0
+                    raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
0
+                  end
0
+                instance_variable_set("@#{name}", object)
0
               end
0
               instance_variable_get("@#{name}")
0
             end
0
@@ -170,14 +190,23 @@ module ActiveRecord
0
 
0
         end
0
 
0
-        def writer_method(name, class_name, mapping, allow_nil, conversion)
0
+        def writer_method(name, class_name, mapping, allow_nil, converter)
0
           module_eval do
0
             define_method("#{name}=") do |part|
0
               if part.nil? && allow_nil
0
                 mapping.each { |pair| self[pair.first] = nil }
0
                 instance_variable_set("@#{name}", nil)
0
               else
0
-                part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
0
+                unless part.is_a?(class_name.constantize) || converter.nil?
0
+                  part = case converter
0
+                    when Symbol
0
+                     class_name.constantize.send(converter, part)
0
+                    when Proc, Method
0
+                      converter.call(part)
0
+                    else
0
+                      raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
0
+                    end
0
+                end
0
                 mapping.each { |pair| self[pair.first] = part.send(pair.last) }
0
                 instance_variable_set("@#{name}", part.freeze)
0
               end
...
107
108
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
111
112
...
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
0
@@ -107,6 +107,41 @@ class AggregationsTest < ActiveRecord::TestCase
0
     customers(:david).gps_location = nil
0
     assert_equal nil, customers(:david).gps_location
0
   end
0
+
0
+  def test_custom_constructor
0
+    assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s
0
+    assert_kind_of Fullname, customers(:barney).fullname
0
+  end
0
+
0
+  def test_custom_converter
0
+    customers(:barney).fullname = 'Barnoit Gumbleau'
0
+    assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s
0
+    assert_kind_of Fullname, customers(:barney).fullname
0
+  end
0
+end
0
+
0
+class DeprecatedAggregationsTest < ActiveRecord::TestCase
0
+  class Person < ActiveRecord::Base; end
0
+
0
+  def test_conversion_block_is_deprecated
0
+    assert_deprecated 'conversion block has been deprecated' do
0
+      Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
0
+    end
0
+  end
0
+
0
+  def test_conversion_block_used_when_converter_option_is_nil
0
+    Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
0
+    assert_raise(NoMethodError) { Person.new.balance = 5 }
0
+  end
0
+
0
+  def test_converter_option_overrides_conversion_block
0
+    Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money }
0
+
0
+    person = Person.new
0
+    assert_nothing_raised { person.balance = 5 }
0
+    assert_equal 5, person.balance.amount
0
+    assert_kind_of Money, person.balance
0
+  end
0
 end
0
 
0
 class OverridingAggregationsTest < ActiveRecord::TestCase
...
6
7
8
9
 
10
11
12
...
14
15
16
 
 
 
 
 
 
 
 
 
17
18
...
6
7
8
 
9
10
11
12
...
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0
@@ -6,7 +6,7 @@ david:
0
   address_city: Scary Town
0
   address_country: Loony Land
0
   gps_location: 35.544623640962634x-105.9309951055148
0
-  
0
+
0
 zaphod:
0
   id: 2
0
   name: Zaphod
0
@@ -14,4 +14,13 @@ zaphod:
0
   address_street: Avenue Road
0
   address_city: Hamlet Town
0
   address_country: Nation Land
0
+  gps_location: NULL
0
+
0
+barney:
0
+  id: 3
0
+  name: Barney Gumble
0
+  balance: 1
0
+  address_street: Quiet Road
0
+  address_city: Peaceful Town
0
+  address_country: Tranquil Land
0
   gps_location: NULL
0
\ No newline at end of file
...
1
2
3
 
4
 
5
6
7
...
53
54
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
...
1
2
 
3
4
5
6
7
8
...
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
0
@@ -1,7 +1,8 @@
0
 class Customer < ActiveRecord::Base
0
   composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
0
-  composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
0
+  composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
0
   composed_of :gps_location, :allow_nil => true
0
+  composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse
0
 end
0
 
0
 class Address
0
@@ -53,3 +54,20 @@ class GpsLocation
0
     self.latitude == other.latitude && self.longitude == other.longitude
0
   end
0
 end
0
+
0
+class Fullname
0
+  attr_reader :first, :last
0
+
0
+  def self.parse(str)
0
+    return nil unless str
0
+    new(*str.to_s.split)
0
+  end
0
+
0
+  def initialize(first, last = nil)
0
+    @first, @last = first, last
0
+  end
0
+
0
+  def to_s
0
+    "#{first} #{last.upcase}"
0
+  end
0
+end
0
\ No newline at end of file

Comments