module ActiveRecord::Aggregations::ClassMethods

def composed_of(part_id, options = {})


converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
mapping: %w(ip to_i),
class_name: 'IPAddr',
composed_of :ip_address,
composed_of :gps_location, allow_nil: true
composed_of :gps_location
composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
composed_of :balance, class_name: "Money", mapping: %w(balance amount)
composed_of :temperature, mapping: %w(reading celsius)
Option examples:

can return +nil+ to skip the assignment.
not an instance of :class_name. If :allow_nil is set to true, the converter
passed the single value that is used in the assignment and is only called if the new value is
or a Proc that is called when a new value is assigned to the value object. The converter is
* :converter - A symbol specifying the name of a class method of :class_name
The default is :new.
to instantiate a :class_name object.
in the order that they are defined in the :mapping option, as arguments and uses them
is called to initialize the value object. The constructor is passed all of the mapped attributes,
* :constructor - A symbol specifying the name of the constructor method or a Proc that
This defaults to +false+.
mapped attributes.
attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
* :allow_nil - Specifies that the value object will not be instantiated when all mapped
value class constructor.
order in which mappings are defined determines the order in which attributes are sent to the
entity attribute and the second item is the name of the attribute in the value object. The
object. Each mapping is represented as an array where the first item is the name of the
* :mapping - Specifies the mapping of entity attributes to attributes of the value
with this option.
to the Address class, but if the real class name is +CompanyAddress+, you'll have to specify it
can't be inferred from the part id. So composed_of :address will by default be linked
* :class_name - Specifies the class name of the association. Use it only if that name
Options are:

composed_of :address adds address and address=(new_address) methods.
Adds reader and writer methods for manipulating a value object:
def composed_of(part_id, options = {})
  options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
  unless self < Aggregations
    include Aggregations
  end
  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
  constructor = options[:constructor] || :new
  converter   = options[:converter]
  reader_method(name, class_name, mapping, allow_nil, constructor)
  writer_method(name, class_name, mapping, allow_nil, converter)
  reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
  Reflection.add_aggregate_reflection self, part_id, reflection
end

def reader_method(name, class_name, mapping, allow_nil, constructor)

def reader_method(name, class_name, mapping, allow_nil, constructor)
  define_method(name) do
    if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? { |key, _| !read_attribute(key).nil? })
      attrs = mapping.collect { |key, _| read_attribute(key) }
      object = constructor.respond_to?(:call) ?
        constructor.call(*attrs) :
        class_name.constantize.send(constructor, *attrs)
      @aggregation_cache[name] = object
    end
    @aggregation_cache[name]
  end
end

def writer_method(name, class_name, mapping, allow_nil, converter)

def writer_method(name, class_name, mapping, allow_nil, converter)
  define_method("#{name}=") do |part|
    klass = class_name.constantize
    unless part.is_a?(klass) || converter.nil? || part.nil?
      part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
    end
    hash_from_multiparameter_assignment = part.is_a?(Hash) &&
      part.keys.all?(Integer)
    if hash_from_multiparameter_assignment
      raise ArgumentError unless part.size == part.each_key.max
      part = klass.new(*part.sort.map(&:last))
    end
    if part.nil? && allow_nil
      mapping.each { |key, _| write_attribute(key, nil) }
      @aggregation_cache[name] = nil
    else
      mapping.each { |key, value| write_attribute(key, part.send(value)) }
      @aggregation_cache[name] = part.freeze
    end
  end
end