Forked from mhuggins/multiparameter_attribute_assignment.rb
Last active
August 29, 2015 14:15
-
-
Save yctay/751c5c5c19c94f892fd2 to your computer and use it in GitHub Desktop.
Revisions
-
mhuggins revised this gist
Dec 4, 2014 . 1 changed file with 3 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,11 @@ # app/models/sample_class.rb class SampleClass include ActiveModel::Model include MultiparameterAttributeAssignment attr_accessor :name, :created_at, :updated_at def self.class_for_attribute(name) Time if %w[created_at updated_at].include?(name) end -
mhuggins renamed this gist
Dec 4, 2014 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ # app/models/sample_class.rb class SampleClass include MultiparameterAttributeAssignment -
mhuggins revised this gist
Dec 4, 2014 . 2 changed files with 7 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,7 @@ class SampleClass include MultiparameterAttributeAssignment def self.class_for_attribute(name) Time if %w[created_at updated_at].include?(name) end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,9 +1,6 @@ # app/models/concerns/multiparameter_attribute_assignment.rb module MultiparameterAttributeAssignment include ActiveModel::ForbiddenAttributesProtection def initialize(params = {}) -
mhuggins created this gist
Jan 20, 2014 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,199 @@ # app/models/concerns/multiparameter_attribute_assignment.rb require 'active_support/concern' module MultiparameterAttributeAssignment extend ActiveSupport::Concern include ActiveModel::ForbiddenAttributesProtection def initialize(params = {}) assign_attributes(params) end def assign_attributes(new_attributes) multi_parameter_attributes = [] attributes = sanitize_for_mass_assignment(new_attributes.stringify_keys) attributes.each do |k, v| if k.include?('(') multi_parameter_attributes << [ k, v ] else send("#{k}=", v) end end assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end alias attributes= assign_attributes protected def attribute_assignment_error_class ActiveModel::AttributeAssignmentError end def multiparameter_assignment_errors_class ActiveModel::MultiparameterAssignmentErrors end def unknown_attribute_error_class ActiveModel::UnknownAttributeError end def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) ) end def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin raise unknown_attribute_error_class, "unknown attribute: #{name}" unless respond_to?("#{name}=") send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex errors << attribute_assignment_error_class.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end end unless errors.empty? error_descriptions = errors.map { |ex| ex.message }.join(',') raise multiparameter_assignment_errors_class.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end def extract_callstack_for_multiparameter_attributes(pairs) attributes = {} pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split('(').first 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 end attributes end def type_cast_attribute_value(multiparameter_name, value) multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_#{$1}") : value end def find_parameter_position(multiparameter_name) multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end end class MultiparameterAttribute attr_reader :object, :name, :values def initialize(object, name, values) @object = object @name = name @values = values end def class_for_attribute object.class_for_attribute(name) end def read_value return if values.values.compact.empty? klass = class_for_attribute if klass.nil? raise ActiveModel::UnexpectedMultiparameterValueError, "Did not expect a multiparameter value for #{name}. " + 'You may be passing the wrong value, or you need to modify ' + 'class_for_attribute so that it returns the right class for ' + "#{name}." elsif klass == Time read_time elsif klass == Date read_date else read_other(klass) end end private def instantiate_time_object(set_values) Time.zone.local(*set_values) end def read_time validate_required_parameters!([1,2,3]) return if blank_date_parameter? 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_required_parameters!(positions) set_values = values.values_at(*positions) klass.new(*set_values) end def blank_date_parameter? (1..3).any? { |position| values[position].blank? } end def validate_required_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 class ActiveModel::AttributeAssignmentError < StandardError attr_reader :exception, :attribute def initialize(message, exception, attribute) super(message) @exception = exception @attribute = attribute end end class ActiveModel::MultiparameterAssignmentErrors < StandardError attr_reader :errors def initialize(errors) @errors = errors end end class ActiveModel::UnexpectedMultiparameterValueError < StandardError end class ActiveModel::UnknownAttributeError < NoMethodError end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,75 @@ # spec/support/examples/multiparameter_attributes.rb shared_examples_for 'a model with multiparameter date attributes' do |factory, *attributes| subject { build factory } attributes.each do |attribute| it 'should assign date when all date parts are valid' do subject.assign_attributes(multiparameter_date(attribute, 2006, 12, 1)) expect( subject.send(attribute) ).to eq Date.new(2006, 12, 1) end it 'should not assign date when missing year part' do subject.assign_attributes(multiparameter_date(attribute, nil, 12, 1)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing month part' do subject.assign_attributes(multiparameter_date(attribute, 2006, nil, 1)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing day part' do subject.assign_attributes(multiparameter_date(attribute, 2006, 12, nil)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing year and month parts' do subject.assign_attributes(multiparameter_date(attribute, nil, nil, 1)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing year and day parts' do subject.assign_attributes(multiparameter_date(attribute, nil, 12, nil)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing month and day parts' do subject.assign_attributes(multiparameter_date(attribute, 2006, nil, nil)) expect( subject.send(attribute) ).to be_nil end it 'should not assign date when missing all date parts' do subject.assign_attributes(multiparameter_date(attribute, nil, nil, nil)) expect( subject.send(attribute) ).to be_nil end it 'should raise error when month part is invalid' do expect { subject.assign_attributes(multiparameter_date(attribute, 2006, 99, 1)) }.to raise_error ActiveModel::MultiparameterAssignmentErrors end it 'should raise error when day part is invalid' do expect { subject.assign_attributes(multiparameter_date(attribute, 2006, 12, 99)) }.to raise_error ActiveModel::MultiparameterAssignmentErrors end end it 'should raise error when assigning to invalid attribute' do expect { subject.assign_attributes(multiparameter_date(:some_unreasonably_existing_attribute, 2006, 12, 1)) }.to raise_error ActiveModel::MultiparameterAssignmentErrors end private def multiparameter_date(attribute, year, month, day) { "#{attribute}(1i)" => year.to_s, "#{attribute}(2i)" => month.to_s, "#{attribute}(3i)" => day.to_s, } end end