require 'active_support/core_ext/module/aliasing' module Rails module DataMapper # Encapsulates an error of a multiparameter assignment. class MultiparameterAssignmentError < StandardError # Gets the target attribute of the assignment. # @return [String] attr_reader :attribute # Gets the assigned values. # @return [Array] attr_reader :values # Gets the exception raised on the assignment. # @return [Exception] attr_reader :exception # Initializes a new instance of the {MultiparameterAssignmentError} class. # # @param [String] attribute The target attribute of the assignment. # @param [Array] values The assigned values. # @param [Exception] exception The exception raised on the assignment. def initialize(attribute, values, exception) super("Could not assign #{values.inspect} to #{attribute} (#{exception.message}).") @attribute = attribute @values = values @exception = exception end end # Raised by {MultiparameterAttributes#attributes=} when there are errors when # merging multiparameter attributes. Use {#errors} to get the array of errors # occured. class MultiparameterAssignmentErrors < StandardError # Gets the array of assignment errors. # @return [Array] attr_reader :errors # Initializes a new instance of the {MultiparameterAssignmentErrors} class. # # @param [Array] errors # The array of assignment errors. # @param [String] message Optional error message. def initialize(errors, message = nil) super(message || "#{errors.size} error#{errors.size == 1 ? '' : 's'} on assignment of multiparameter attributes.") @errors = errors end end # Include this module into a DataMapper model to enable multiparameter # attributes. # # A multiparameter attribute has +attr(Xc)+ as name where +attr+ specifies # the attribute, +X+ the position of the value, and +c+ an optional typecast # identifier. All values that share an +attr+ are sorted by their position, # optionally cast using +#to_c+ (where +c+ is the typecast identifier) and # then used to instantiate the proper attribute value. # # @example # # Assigning a hash with multiparameter values for a +Date+ property called # # +written_on+: # resource.attributes = { # 'written_on(1i)' => '2004', # 'written_on(2i)' => '6', # 'written_on(3i)' => '24' } # # # +Date+ will be initialized with each string cast to a number using # # #to_i. # resource.written_on == Date.new(2004, 6, 24) module MultiparameterAttributes # Merges multiparameter attributes and calls +super+ with the merged # attributes. # # @param [Hash{String,Symbol=>Object}] attributes # Names and values of attributes to assign. # @return [Hash] # Names and values of attributes assigned. # @raise [MultiparameterAssignmentErrors] # One or more multiparameters could not be assigned. # @api public def attributes=(attributes) attribs = attributes.dup multi_parameter_attributes = [] attribs.each do |k, v| if k.to_s.include?("(") multi_parameter_attributes << [ k, v ] attribs.delete(k) end end attribs.merge!(merge_multiparameter_attributes(multi_parameter_attributes)) super(attribs) end protected def merge_multiparameter_attributes(pairs) pairs = extract_multiparameter_attributes(pairs) errors = [] attributes = {} pairs.each do |name, values_with_empty_parameters| # ActiveRecord keeps the empty values to set dates without a year. # Removing all nils (instead of removing only trailing nils) seems # like a weird behavior though. values = values_with_empty_parameters.compact if values.empty? attributes[name] = nil next end klass = properties[name].primitive begin attributes[name] = if klass == Time Time.local(*values) elsif klass == Date # Date does not replace nil values with defaults. Date.new(*values_with_empty_parameters.map { |v| v.nil? ? 1 : v }) else klass.new(*values) end rescue => ex errors << MultiparameterAssignmentError.new(name, values, ex) end end unless errors.empty? raise MultiparameterAssignmentErrors.new(errors) end attributes end def extract_multiparameter_attributes(pairs) attributes = {} for multiparameter_name, value in pairs unless multiparameter_name =~ /\A ([^\)]+) \( ([0-9]+) ([a-z])? \) \z/x raise "Invalid multiparameter name #{multiparameter_name.inspect}." end name, position, typecast = $1, $2, $3 attributes[name] ||= [] parameter_value = if value.empty? nil elsif typecast value.send('to_' + typecast) else value end attributes[name] << [ position, parameter_value ] end # Order each parameter array according to the position, then discard the # position. attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } } end end # MultiparameterAttributes end end