diff --git a/lib/active_attr.rb b/lib/active_attr.rb index 3fc22e1..f347023 100644 --- a/lib/active_attr.rb +++ b/lib/active_attr.rb @@ -18,6 +18,7 @@ module ActiveAttr autoload :Logger autoload :MassAssignment autoload :MassAssignmentSecurity + autoload :MultiAttr autoload :Model autoload :QueryAttributes autoload :TypecastedAttributes diff --git a/lib/active_attr/mass_assignment.rb b/lib/active_attr/mass_assignment.rb index 960f739..7bcac22 100644 --- a/lib/active_attr/mass_assignment.rb +++ b/lib/active_attr/mass_assignment.rb @@ -29,10 +29,49 @@ module MassAssignment # # @since 0.1.0 def assign_attributes(new_attributes, options={}) - new_attributes.each do |name, value| - writer = "#{name}=" - send writer, value if respond_to? writer - end if new_attributes + if new_attributes + prepare_attributes(new_attributes).each do |name, value| + writer = "#{name}=" + send writer, value if respond_to? writer + end + end + end + + def prepare_attributes(attrs) + multi_attr_pairs = [] + + attrs.inject({}) do |res, (name, value)| + if name.to_s.include?("(") + multi_attr_pairs << [name, value] + else + res[name] = value + end + + res + end.merge(prepare_multi_attrs(multi_attr_pairs)) + end + + def prepare_multi_attrs(pairs) + attributes = {} + + pairs.each do |pair| + multiparameter_name, value = pair + attribute_name = multiparameter_name.split("(").first + attributes[attribute_name] = {} unless attributes.include?(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.inject({}) { |res, (k, v)| res[k] = ActiveAttr::MultiAttr.new(v); res } + 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 # Mass update a model's attributes diff --git a/lib/active_attr/multi_attr.rb b/lib/active_attr/multi_attr.rb new file mode 100644 index 0000000..c609506 --- /dev/null +++ b/lib/active_attr/multi_attr.rb @@ -0,0 +1,48 @@ +module ActiveAttr + class MultiAttr + class MissingParameter < Exception; end + + attr_accessor :hash + delegate :[], :has_key?, :to => :hash + + def initialize(hash) + self.hash = hash + end + + def to_time + # If Date bits were not provided, error + raise MissingParameter if [1,2,3].any?{|position| !has_key?(position)} + # If Date bits were provided but blank, then return nil + return nil if (1..3).any? {|position| hash[position].blank?} + + set_values = (1..max_position).collect{|position| hash[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(set_values) + end + + def to_date + return nil if (1..3).any? {|position| hash[position].blank?} + set_values = [hash[1], hash[2], hash[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 max_position(upper_cap = 100) + [hash.keys.max, upper_cap].min + end + + private + + def instantiate_time_object(values) + if Time.respond_to?(:zone) && Time.zone + Time.zone.local(*values) + else + Time.local(*values) + end + end + end +end diff --git a/lib/active_attr/typecasted_attributes.rb b/lib/active_attr/typecasted_attributes.rb index 05894e4..9935ac3 100644 --- a/lib/active_attr/typecasted_attributes.rb +++ b/lib/active_attr/typecasted_attributes.rb @@ -55,7 +55,13 @@ def attribute_before_type_cast(name) # # @since 0.5.0 def attribute(name) - typecast_attribute(_attribute_typecaster(name), super) + value = super + + if value.is_a?(ActiveAttr::MultiAttr) + typecast_multiattr(_attribute_type(name), value) + else + typecast_attribute(_attribute_typecaster(name), value) + end end # Calculates an attribute type diff --git a/lib/active_attr/typecasting.rb b/lib/active_attr/typecasting.rb index 92baf36..12f8b48 100644 --- a/lib/active_attr/typecasting.rb +++ b/lib/active_attr/typecasting.rb @@ -8,6 +8,7 @@ require "active_attr/typecasting/object_typecaster" require "active_attr/typecasting/string_typecaster" require "active_attr/typecasting/unknown_typecaster_error" +require "active_attr/multi_attr" module ActiveAttr # Typecasting provides methods to typecast a value to a different type @@ -60,5 +61,19 @@ def typecaster_for(type) typecaster.new if typecaster end + + def typecast_multiattr(klass, value) + if klass == Time + value.to_time + elsif klass == Date + value.to_date + else + values = (1..value.max_position).collect do |position| + raise ActiveAttr::MultiAttr::MissingParameter if !value.has_key?(position) + value[position] + end + klass.new(*values) + end + end end end diff --git a/spec/unit/active_attr/mass_assignment_spec.rb b/spec/unit/active_attr/mass_assignment_spec.rb index be5a6c5..79aa388 100644 --- a/spec/unit/active_attr/mass_assignment_spec.rb +++ b/spec/unit/active_attr/mass_assignment_spec.rb @@ -23,6 +23,13 @@ module ActiveAttr person = mass_assign_attributes(:middle_name => "J") person.middle_name.should be_nil end + + it "creates multi attr" do + person = mass_assign_attributes('first_name(1i)' => '100', 'first_name(2s)' => 'test') + person.first_name.should be_a(ActiveAttr::MultiAttr) + person.first_name.hash[1].should == 100 + person.first_name.hash[2].should == 'test' + end end describe "#assign_attributes", :assign_attributes, :lenient_mass_assignment_method diff --git a/spec/unit/active_attr/typecasted_attributes_spec.rb b/spec/unit/active_attr/typecasted_attributes_spec.rb index 2e2abd8..5241e95 100644 --- a/spec/unit/active_attr/typecasted_attributes_spec.rb +++ b/spec/unit/active_attr/typecasted_attributes_spec.rb @@ -43,6 +43,51 @@ def self.name subject.should_receive(:attribute_before_type_cast).with("last_name") subject.last_name_before_type_cast end + + context "MultiValue" do + before do + model_class.class_eval do + attribute :time, :type => Time + attribute :date, :type => Date + end + end + + it "typecasts MultiValue to time" do + subject.time = ActiveAttr::MultiAttr.new(1 => "2010", 2 => "10", 3 => "5") + subject.time.should == Time.local(2010, 10, 5) + end + + it "typecasts MultiValue to date" do + subject.date = ActiveAttr::MultiAttr.new(1 => "2010", 2 => "10", 3 => "5") + subject.date.should == Date.new(2010, 10, 5) + end + + context "custom class" do + it "typecasts MultiValue to custom class" do + custom_class = Class.new do + attr_accessor :a, :b + def initialize(a, b) + self.a = a + self.b = b + end + end + model_class.class_eval do + attribute :custom, :type => custom_class + end + + subject.custom = MultiAttr.new(1 => "foo", 2 => "bar") + result = subject.custom + result.should be_a(custom_class) + result.a.should == "foo" + result.b.should == "bar" + end + end + + it "raises ActiveAttr::MultiAttr::MissingParameter on arguments error" do + subject.time = MultiAttr.new(1 => "2010", 3 => "5") + expect { subject.time }.to raise_error(ActiveAttr::MultiAttr::MissingParameter) + end + end end describe ".inspect" do