Index: activerecord/test/fixtures/customer.rb
===================================================================
--- activerecord/test/fixtures/customer.rb (revision 5212)
+++ activerecord/test/fixtures/customer.rb (working copy)
@@ -1,6 +1,8 @@
+require 'money'
+
class Customer < ActiveRecord::Base
composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
- composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
composed_of :gps_location, :allow_nil => true
end
Index: activerecord/test/aggregations_test.rb
===================================================================
--- activerecord/test/aggregations_test.rb (revision 5212)
+++ activerecord/test/aggregations_test.rb (working copy)
@@ -92,4 +92,23 @@
def test_nil_raises_error_when_allow_nil_is_false
assert_raises(NoMethodError) { customers(:david).balance = nil }
end
+
+ def test_allow_nil_address_set_to_nil_returns_nil
+ assert_not_nil customers(:zaphod).address
+ customers(:zaphod).address = nil
+ assert_nil customers(:zaphod).address
+ end
+
+ def test_conversion
+ assert_nothing_raised { customers(:david).balance = "10.00" }
+ assert_equal Money.new(10), customers(:david).balance
+ end
+
+ def test_allow_nil_address_loaded_when_only_some_attributes_are_nil
+ customers(:zaphod).address_street = nil
+ customers(:zaphod).save
+ customers(:zaphod).reload
+ assert customers(:zaphod).address.is_a?(Address)
+ assert customers(:zaphod).address.street.nil?
+ end
end
Index: activerecord/lib/active_record/aggregations.rb
===================================================================
--- activerecord/lib/active_record/aggregations.rb (revision 5212)
+++ activerecord/lib/active_record/aggregations.rb (working copy)
@@ -121,69 +121,58 @@
# * :allow_nil - specifies that the aggregate object will not be instantiated when all mapped
# attributes are nil. Setting the aggregate class to nil has the effect of writing nil to all mapped attributes.
# This defaults to false.
+ #
+ # An optional block can be passed to convert the argument that is passed to the writer method into an instance of
+ # :class_name. The block will only be called if the arguement is not already an instance of :class_name.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, :allow_nil => true
#
- def composed_of(part_id, options = {})
+ def composed_of(part_id, options = {}, &block)
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
name = part_id.id2name
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
allow_nil = options[:allow_nil] || false
reader_method(name, class_name, mapping, allow_nil)
- writer_method(name, class_name, mapping, allow_nil)
+ writer_method(name, class_name, mapping, allow_nil, block)
create_reflection(:composed_of, part_id, options, self)
end
private
def reader_method(name, class_name, mapping, allow_nil)
- mapping = (Array === mapping.first ? mapping : [ mapping ])
-
- allow_nil_condition = if allow_nil
- mapping.collect { |pair| "!read_attribute(\"#{pair.first}\").nil?"}.join(" && ")
- else
- "true"
- end
-
- module_eval <<-end_eval
- def #{name}(force_reload = false)
- if (@#{name}.nil? || force_reload) && #{allow_nil_condition}
- @#{name} = #{class_name}.new(#{mapping.collect { |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
+ module_eval do
+ define_method(name) do |*args|
+ force_reload = args.first || false
+ if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
+ instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
end
- return @#{name}
+ return instance_variable_get("@#{name}")
end
- end_eval
- end
-
- def writer_method(name, class_name, mapping, allow_nil)
- mapping = (Array === mapping.first ? mapping : [ mapping ])
+ end
- if allow_nil
- module_eval <<-end_eval
- def #{name}=(part)
- if part.nil?
- #{mapping.collect { |pair| "@attributes[\"#{pair.first}\"] = nil" }.join("\n")}
- else
- @#{name} = part.freeze
- #{mapping.collect { |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
- end
+ end
+
+ def writer_method(name, class_name, mapping, allow_nil, conversion)
+ module_eval do
+ define_method("#{name}=") do |part|
+ if part.nil? && allow_nil
+ mapping.each { |pair| @attributes[pair.first] = nil }
+ instance_variable_set("@#{name}", nil)
+ else
+ part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
+ mapping.each { |pair| @attributes[pair.first] = part.send(pair.last) }
+ instance_variable_set("@#{name}", part.freeze)
end
- end_eval
- else
- module_eval <<-end_eval
- def #{name}=(part)
- @#{name} = part.freeze
- #{mapping.collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
- end
- end_eval
+ end
end
end
end