Skip to content
This repository
Browse code

Merge branch 'multiparameter-attributes-refactor'

Refactor multiparameter attributes assignment implementation.
  • Loading branch information...
commit db78e58294c5e4ee6fb960c79f882c80b22afbcf 2 parents 9f88521 + ec01242
Carlos Antonio da Silva carlosantoniodasilva authored
179 activerecord/lib/active_record/attribute_assignment.rb
@@ -104,8 +104,7 @@ def assign_attributes(new_attributes, options = {})
104 104 end
105 105 end
106 106
107   - # assign any deferred nested attributes after the base attributes have been set
108   - nested_parameter_attributes.each { |k,v| _assign_attribute(k, v) }
  107 + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
109 108 assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
110 109 ensure
111 110 @mass_assignment_options = previous_options
@@ -133,6 +132,11 @@ def _assign_attribute(k, v)
133 132 end
134 133 end
135 134
  135 + # Assign any deferred nested attributes after the base attributes have been set.
  136 + def assign_nested_parameter_attributes(pairs)
  137 + pairs.each { |k, v| _assign_attribute(k, v) }
  138 + end
  139 +
136 140 # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
137 141 # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
138 142 # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
@@ -146,19 +150,11 @@ def assign_multiparameter_attributes(pairs)
146 150 )
147 151 end
148 152
149   - def instantiate_time_object(name, values)
150   - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
151   - Time.zone.local(*values)
152   - else
153   - Time.time_with_datetime_fallback(self.class.default_timezone, *values)
154   - end
155   - end
156   -
157 153 def execute_callstack_for_multiparameter_attributes(callstack)
158 154 errors = []
159 155 callstack.each do |name, values_with_empty_parameters|
160 156 begin
161   - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
  157 + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
162 158 rescue => ex
163 159 errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
164 160 end
@@ -169,74 +165,12 @@ def execute_callstack_for_multiparameter_attributes(callstack)
169 165 end
170 166 end
171 167
172   - def read_value_from_parameter(name, values_hash_from_param)
173   - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
174   - if values_hash_from_param.values.all?{|v|v.nil?}
175   - nil
176   - elsif klass == Time
177   - read_time_parameter_value(name, values_hash_from_param)
178   - elsif klass == Date
179   - read_date_parameter_value(name, values_hash_from_param)
180   - else
181   - read_other_parameter_value(klass, name, values_hash_from_param)
182   - end
183   - end
184   -
185   - def read_time_parameter_value(name, values_hash_from_param)
186   - # If column is a :time (and not :date or :timestamp) there is no need to validate if
187   - # there are year/month/day fields
188   - if column_for_attribute(name).type == :time
189   - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
190   - {1 => 1970, 2 => 1, 3 => 1}.each do |key,value|
191   - values_hash_from_param[key] ||= value
192   - end
193   - else
194   - # else column is a timestamp, so if Date bits were not provided, error
195   - if missing_parameter = [1,2,3].detect{ |position| !values_hash_from_param.has_key?(position) }
196   - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter}i)")
197   - end
198   -
199   - # If Date bits were provided but blank, then return nil
200   - return nil if (1..3).any? { |position| values_hash_from_param[position].blank? }
201   - end
202   -
203   - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
204   - set_values = (1..max_position).collect{ |position| values_hash_from_param[position] }
205   - # If Time bits are not there, then default to 0
206   - (3..5).each { |i| set_values[i] = set_values[i].blank? ? 0 : set_values[i] }
207   - instantiate_time_object(name, set_values)
208   - end
209   -
210   - def read_date_parameter_value(name, values_hash_from_param)
211   - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
212   - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
213   - begin
214   - Date.new(*set_values)
215   - rescue ArgumentError # if Date.new raises an exception on an invalid date
216   - 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
217   - end
218   - end
219   -
220   - def read_other_parameter_value(klass, name, values_hash_from_param)
221   - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
222   - values = (1..max_position).collect do |position|
223   - raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
224   - values_hash_from_param[position]
225   - end
226   - klass.new(*values)
227   - end
228   -
229   - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
230   - [values_hash_from_param.keys.max,upper_cap].min
231   - end
232   -
233 168 def extract_callstack_for_multiparameter_attributes(pairs)
234 169 attributes = { }
235 170
236   - pairs.each do |pair|
237   - multiparameter_name, value = pair
  171 + pairs.each do |(multiparameter_name, value)|
238 172 attribute_name = multiparameter_name.split("(").first
239   - attributes[attribute_name] = {} unless attributes.include?(attribute_name)
  173 + attributes[attribute_name] ||= {}
240 174
241 175 parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
242 176 attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
@@ -253,5 +187,100 @@ def find_parameter_position(multiparameter_name)
253 187 multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
254 188 end
255 189
  190 + class MultiparameterAttribute #:nodoc:
  191 + attr_reader :object, :name, :values, :column
  192 +
  193 + def initialize(object, name, values)
  194 + @object = object
  195 + @name = name
  196 + @values = values
  197 + end
  198 +
  199 + def read_value
  200 + return if values.values.compact.empty?
  201 +
  202 + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
  203 + klass = column.klass
  204 +
  205 + if klass == Time
  206 + read_time
  207 + elsif klass == Date
  208 + read_date
  209 + else
  210 + read_other(klass)
  211 + end
  212 + end
  213 +
  214 + private
  215 +
  216 + def instantiate_time_object(set_values)
  217 + if object.class.send(:create_time_zone_conversion_attribute?, name, column)
  218 + Time.zone.local(*set_values)
  219 + else
  220 + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
  221 + end
  222 + end
  223 +
  224 + def read_time
  225 + # If column is a :time (and not :date or :timestamp) there is no need to validate if
  226 + # there are year/month/day fields
  227 + if column.type == :time
  228 + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
  229 + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
  230 + values[key] ||= value
  231 + end
  232 + else
  233 + # else column is a timestamp, so if Date bits were not provided, error
  234 + validate_missing_parameters!([1,2,3])
  235 +
  236 + # If Date bits were provided but blank, then return nil
  237 + return if blank_date_parameter?
  238 + end
  239 +
  240 + max_position = extract_max_param(6)
  241 + set_values = values.values_at(*(1..max_position))
  242 + # If Time bits are not there, then default to 0
  243 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
  244 + instantiate_time_object(set_values)
  245 + end
  246 +
  247 + def read_date
  248 + return if blank_date_parameter?
  249 + set_values = values.values_at(1,2,3)
  250 + begin
  251 + Date.new(*set_values)
  252 + rescue ArgumentError # if Date.new raises an exception on an invalid date
  253 + 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
  254 + end
  255 + end
  256 +
  257 + def read_other(klass)
  258 + max_position = extract_max_param
  259 + positions = (1..max_position)
  260 + validate_missing_parameters!(positions)
  261 +
  262 + set_values = values.values_at(*positions)
  263 + klass.new(*set_values)
  264 + end
  265 +
  266 + # Checks whether some blank date parameter exists. Note that this is different
  267 + # than the validate_missing_parameters! method, since it just checks for blank
  268 + # positions instead of missing ones, and does not raise in case one blank position
  269 + # exists. The caller is responsible to handle the case of this returning true.
  270 + def blank_date_parameter?
  271 + (1..3).any? { |position| values[position].blank? }
  272 + end
  273 +
  274 + # If some position is not provided, it errors out a missing parameter exception.
  275 + def validate_missing_parameters!(positions)
  276 + if missing_parameter = positions.detect { |position| !values.key?(position) }
  277 + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
  278 + end
  279 + end
  280 +
  281 + def extract_max_param(upper_cap = 100)
  282 + [values.keys.max, upper_cap].min
  283 + end
  284 + end
256 285 end
257 286 end
1  activerecord/test/cases/attribute_methods_test.rb
@@ -34,7 +34,6 @@ def test_attribute_present
34 34 assert t.attribute_present?("written_on")
35 35 assert !t.attribute_present?("content")
36 36 assert !t.attribute_present?("author_name")
37   -
38 37 end
39 38
40 39 def test_attribute_present_with_booleans

2 comments on commit db78e58

Dmitry Polushkin

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

Rafael Mendonça França
Owner

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.
Something went wrong with that request. Please try again.