0
@@ -10,10 +10,10 @@ module ActiveRecord
0
end unless self.new_record?
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
# class Customer < ActiveRecord::Base
0
@@ -30,10 +30,10 @@ module ActiveRecord
0
# attr_reader :amount, :currency
0
- # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
0
- # def initialize(amount, currency = "USD")
0
- # @amount, @currency = amount, currency
0
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
0
+ # def initialize(amount, currency = "USD")
0
+ # @amount, @currency = amount, currency
0
# def exchange_to(other_currency)
0
@@ -56,19 +56,19 @@ module ActiveRecord
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
- # def close_to?(other_address)
0
- # city == other_address.city
0
+ # def close_to?(other_address)
0
+ # city == other_address.city
0
# def ==(other_address)
0
# city == other_address.city && street == other_address.street
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
# == Writing value objects
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
- # 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
# 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
@@ -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
- # 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
# 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
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
- 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
+ ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
0
+ reader_method(name, class_name, mapping, allow_nil, constructor)
0
+ writer_method(name, class_name, mapping, allow_nil, converter)
0
- reader_method(name, class_name, mapping, allow_nil)
0
- writer_method(name, class_name, mapping, allow_nil, block)
0
create_reflection(:composed_of, part_id, options, self)
0
- def reader_method(name, class_name, mapping, allow_nil
)
0
+ def reader_method(name, class_name, mapping, allow_nil
, constructor)
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
+ class_name.constantize.send(constructor, *attrs)
0
+ constructor.call(*attrs)
0
+ raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
0
+ instance_variable_set("@#{name}", object)
0
instance_variable_get("@#{name}")
0
@@ -170,14 +190,23 @@ module ActiveRecord
0
- def writer_method(name, class_name, mapping, allow_nil, conver
sion)
0
+ def writer_method(name, class_name, mapping, allow_nil, conver
ter)
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
- part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
0
+ unless part.is_a?(class_name.constantize) || converter.nil?
0
+ class_name.constantize.send(converter, part)
0
+ raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
0
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
0
instance_variable_set("@#{name}", part.freeze)