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