Skip to content

Commit

Permalink
Merge branch 'multiparameter-attributes-refactor'
Browse files Browse the repository at this point in the history
Refactor multiparameter attributes assignment implementation.
  • Loading branch information
carlosantoniodasilva committed Aug 18, 2012
2 parents 9f88521 + ec01242 commit db78e58
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 76 deletions.
179 changes: 104 additions & 75 deletions activerecord/lib/active_record/attribute_assignment.rb
Expand Up @@ -104,8 +104,7 @@ def assign_attributes(new_attributes, options = {})
end
end

# assign any deferred nested attributes after the base attributes have been set
nested_parameter_attributes.each { |k,v| _assign_attribute(k, v) }
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
ensure
@mass_assignment_options = previous_options
Expand Down Expand Up @@ -133,6 +132,11 @@ def _assign_attribute(k, v)
end
end

# Assign any deferred nested attributes after the base attributes have been set.
def assign_nested_parameter_attributes(pairs)
pairs.each { |k, v| _assign_attribute(k, v) }
end

# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
Expand All @@ -146,19 +150,11 @@ def assign_multiparameter_attributes(pairs)
)
end

def instantiate_time_object(name, values)
if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
Time.zone.local(*values)
else
Time.time_with_datetime_fallback(self.class.default_timezone, *values)
end
end

def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
Expand All @@ -169,74 +165,12 @@ def execute_callstack_for_multiparameter_attributes(callstack)
end
end

def read_value_from_parameter(name, values_hash_from_param)
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
if values_hash_from_param.values.all?{|v|v.nil?}
nil
elsif klass == Time
read_time_parameter_value(name, values_hash_from_param)
elsif klass == Date
read_date_parameter_value(name, values_hash_from_param)
else
read_other_parameter_value(klass, name, values_hash_from_param)
end
end

def read_time_parameter_value(name, values_hash_from_param)
# If column is a :time (and not :date or :timestamp) there is no need to validate if
# there are year/month/day fields
if column_for_attribute(name).type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{1 => 1970, 2 => 1, 3 => 1}.each do |key,value|
values_hash_from_param[key] ||= value
end
else
# else column is a timestamp, so if Date bits were not provided, error
if missing_parameter = [1,2,3].detect{ |position| !values_hash_from_param.has_key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter}i)")
end

# If Date bits were provided but blank, then return nil
return nil if (1..3).any? { |position| values_hash_from_param[position].blank? }
end

max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
set_values = (1..max_position).collect{ |position| values_hash_from_param[position] }
# If Time bits are not there, then default to 0
(3..5).each { |i| set_values[i] = set_values[i].blank? ? 0 : set_values[i] }
instantiate_time_object(name, set_values)
end

def read_date_parameter_value(name, values_hash_from_param)
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
begin
Date.new(*set_values)
rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end

def read_other_parameter_value(klass, name, values_hash_from_param)
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
values = (1..max_position).collect do |position|
raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
values_hash_from_param[position]
end
klass.new(*values)
end

def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
[values_hash_from_param.keys.max,upper_cap].min
end

def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }

pairs.each do |pair|
multiparameter_name, value = pair
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
attributes[attribute_name] ||= {}

parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
Expand All @@ -253,5 +187,100 @@ def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end

class MultiparameterAttribute #:nodoc:
attr_reader :object, :name, :values, :column

def initialize(object, name, values)
@object = object
@name = name
@values = values
end

def read_value
return if values.values.compact.empty?

@column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
klass = column.klass

if klass == Time
read_time
elsif klass == Date
read_date
else
read_other(klass)
end
end

private

def instantiate_time_object(set_values)
if object.class.send(:create_time_zone_conversion_attribute?, name, column)
Time.zone.local(*set_values)
else
Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
end
end

def read_time
# If column is a :time (and not :date or :timestamp) there is no need to validate if
# there are year/month/day fields
if column.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
end
else
# else column is a timestamp, so if Date bits were not provided, error
validate_missing_parameters!([1,2,3])

# If Date bits were provided but blank, then return nil
return if blank_date_parameter?
end

max_position = extract_max_param(6)
set_values = values.values_at(*(1..max_position))
# If Time bits are not there, then default to 0
(3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
instantiate_time_object(set_values)
end

def read_date
return if blank_date_parameter?
set_values = values.values_at(1,2,3)
begin
Date.new(*set_values)
rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end

def read_other(klass)
max_position = extract_max_param
positions = (1..max_position)
validate_missing_parameters!(positions)

set_values = values.values_at(*positions)
klass.new(*set_values)
end

# Checks whether some blank date parameter exists. Note that this is different
# than the validate_missing_parameters! method, since it just checks for blank
# positions instead of missing ones, and does not raise in case one blank position
# exists. The caller is responsible to handle the case of this returning true.
def blank_date_parameter?
(1..3).any? { |position| values[position].blank? }
end

# If some position is not provided, it errors out a missing parameter exception.
def validate_missing_parameters!(positions)
if missing_parameter = positions.detect { |position| !values.key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
end
end

def extract_max_param(upper_cap = 100)
[values.keys.max, upper_cap].min
end
end
end
end
1 change: 0 additions & 1 deletion activerecord/test/cases/attribute_methods_test.rb
Expand Up @@ -34,7 +34,6 @@ def test_attribute_present
assert t.attribute_present?("written_on")
assert !t.attribute_present?("content")
assert !t.attribute_present?("author_name")

end

def test_attribute_present_with_booleans
Expand Down

2 comments on commit db78e58

@dmitry
Copy link
Contributor

@dmitry dmitry commented on db78e58 Aug 18, 2012

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not to extract this multiparameter attributes functionality to the ActiveModel?

@rafaelfranca
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is Active Record specific. Others ORM such Mongoid and DataMapper don't need to have the same implementation.

Please sign in to comment.