diff options
-rw-r--r-- | activemodel/CHANGELOG.md | 20 | ||||
-rw-r--r-- | activemodel/lib/active_model.rb | 1 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_assignment.rb | 64 | ||||
-rw-r--r-- | activemodel/test/cases/attribute_assignment_test.rb | 106 | ||||
-rw-r--r-- | activerecord/lib/active_record/attribute_assignment.rb | 57 | ||||
-rw-r--r-- | activerecord/lib/active_record/errors.rb | 16 | ||||
-rw-r--r-- | activerecord/test/cases/associations/has_one_associations_test.rb | 2 | ||||
-rw-r--r-- | activerecord/test/cases/attribute_methods_test.rb | 6 | ||||
-rw-r--r-- | activerecord/test/cases/attributes_test.rb | 2 | ||||
-rw-r--r-- | activerecord/test/cases/nested_attributes_test.rb | 2 |
10 files changed, 220 insertions, 56 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 175912b240..e20ebde8b1 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,23 @@ +* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment` + allowing to use it for any object as an includable module + + ``` ruby + class Cat + include ActiveModel::AttributeAssignment + attr_accessor :name, :status + end + + cat = Cat.new + cat.assign_attributes(name: "Gorby", status: "yawning") + cat.name # => 'Gorby' + cat.status => 'yawning' + cat.assign_attributes(status: "sleeping") + cat.name # => 'Gorby' + cat.status => 'sleeping' + ``` + + *Bogdan Gusiev* + * Add `ActiveModel::Errors#details` To be able to return type of used validator, one can now call `details` diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 46d60db756..58fe08cc11 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -28,6 +28,7 @@ require 'active_model/version' module ActiveModel extend ActiveSupport::Autoload + autoload :AttributeAssignment autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb new file mode 100644 index 0000000000..36ddbad826 --- /dev/null +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -0,0 +1,64 @@ +require 'active_support/core_ext/hash/keys' + +module ActiveModel + module AttributeAssignment + include ActiveModel::ForbiddenAttributesProtection + + # Allows you to set all the attributes by passing in a hash of attributes with + # keys matching the attribute names. + # + # If the passed hash responds to <tt>permitted?</tt> method and the return value + # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> + # exception is raised. + # + # class Cat + # include ActiveModel::AttributeAssignment + # attr_accessor :name, :status + # end + # + # cat = Cat.new + # cat.assign_attributes(name: "Gorby", status: "yawning") + # cat.name # => 'Gorby' + # cat.status => 'yawning' + # cat.assign_attributes(status: "sleeping") + # cat.name # => 'Gorby' + # cat.status => 'sleeping' + def assign_attributes(new_attributes) + if !new_attributes.respond_to?(:stringify_keys) + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + end + return if new_attributes.blank? + + attributes = new_attributes.stringify_keys + _assign_attributes(sanitize_for_mass_assignment(attributes)) + end + + private + def _assign_attributes(attributes) + attributes.each do |k, v| + _assign_attribute(k, v) + end + end + + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError.new(self, k) + end + end + + # Raised when unknown attributes are supplied via mass assignment. + class UnknownAttributeError < NoMethodError + attr_reader :record, :attribute + + def initialize(record, attribute) + @record = record + @attribute = attribute + super("unknown attribute '#{attribute}' for #{@record.class}.") + end + end + end +end diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb new file mode 100644 index 0000000000..5c1be6a456 --- /dev/null +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -0,0 +1,106 @@ +require 'cases/helper' +require 'active_support/hash_with_indifferent_access' + +class AttributeAssignmentTest < ActiveModel::TestCase + + class Model + include ActiveModel::AttributeAssignment + + attr_accessor :name, :description + + def initialize(attributes = {}) + assign_attributes(attributes) + end + + def broken_attribute=(value) + non_existing_method(value) + end + + private + def metadata=(data) + @metadata = data + end + end + + class ProtectedParams < ActiveSupport::HashWithIndifferentAccess + def permit! + @permitted = true + end + + def permitted? + @permitted ||= false + end + + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, permitted? + end + end + end + + test "simple assignment" do + model = Model.new + + model.assign_attributes(name: 'hello', description: 'world') + assert_equal 'hello', model.name + assert_equal 'world', model.description + end + + test "assign non-existing attribute" do + model = Model.new + error = assert_raises ActiveModel::AttributeAssignment::UnknownAttributeError do + model.assign_attributes(hz: 1) + end + + assert_equal model, error.record + assert_equal "hz", error.attribute + end + + test "assign private attribute" do + model = Model.new + assert_raises ActiveModel::AttributeAssignment::UnknownAttributeError do + model.assign_attributes(metadata: { a: 1 }) + end + end + + test "raises NoMethodError if raised in attribute writer" do + assert_raises NoMethodError do + Model.new(broken_attribute: 1) + end + end + + test "raises ArgumentError if non-hash object passed" do + assert_raises ArgumentError do + Model.new(1) + end + end + + test 'forbidden attributes cannot be used for mass assignment' do + params = ProtectedParams.new(name: 'Guille', description: 'm') + assert_raises(ActiveModel::ForbiddenAttributesError) do + Model.new(params) + end + end + + test 'permitted attributes can be used for mass assignment' do + params = ProtectedParams.new(name: 'Guille', description: 'desc') + params.permit! + model = Model.new(params) + + assert_equal 'Guille', model.name + assert_equal 'desc', model.description + end + + test 'regular hash should still be used for mass assignment' do + model = Model.new(name: 'Guille', description: 'm') + + assert_equal 'Guille', model.name + assert_equal 'm', model.description + end + + test 'blank attributes should not raise' do + model = Model.new + assert_nil model.assign_attributes(ProtectedParams.new({})) + end + +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf64830417..e283db0386 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,7 +3,24 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::ForbiddenAttributesProtection + include ActiveModel::AttributeAssignment + + def _assign_attributes(attributes) # :nodoc: + multi_parameter_attributes = {} + nested_parameter_attributes = {} + + attributes.each do |k, v| + if k.include?("(") + multi_parameter_attributes[k] = attributes.delete(k) + elsif v.is_a?(Hash) + nested_parameter_attributes[k] = attributes.delete(k) + end + end + super(attributes) + + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? + end # Allows you to set all the attributes by passing in a hash of attributes with # keys matching the attribute names (which again matches the column names). @@ -19,47 +36,11 @@ module ActiveRecord # # New attributes will be persisted in the database when the object is saved. # - # Aliased to <tt>attributes=</tt>. - def assign_attributes(new_attributes) - if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." - end - return if new_attributes.blank? - - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] - nested_parameter_attributes = [] - - attributes = sanitize_for_mass_assignment(attributes) - - attributes.each do |k, v| - if k.include?("(") - multi_parameter_attributes << [ k, v ] - elsif v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - _assign_attribute(k, v) - end - end - - assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? - assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? - end - + # Aliased to <tt>assign_attributes</tt>. alias attributes= assign_attributes private - def _assign_attribute(k, v) - public_send("#{k}=", v) - rescue NoMethodError - if respond_to?("#{k}=") - raise - else - raise UnknownAttributeError.new(self, k) - 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) } diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc28ab585f..d710d96a9a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -178,18 +178,10 @@ module ActiveRecord class DangerousAttributeError < ActiveRecordError end - # Raised when unknown attributes are supplied via mass assignment. - class UnknownAttributeError < NoMethodError - - attr_reader :record, :attribute - - def initialize(record, attribute) - @record = record - @attribute = attribute.to_s - super("unknown attribute '#{attribute}' for #{@record.class}.") - end - - end + UnknownAttributeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( # :nodoc: + 'ActiveRecord::UnknownAttributeError', + 'ActiveModel::AttributeAssignment::UnknownAttributeError' + ) # Raised when an error occurred while doing a mass assignment to an attribute through the # +attributes=+ method. The exception has an +attribute+ property that is the name of the diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 9b6757e256..4df75adeb4 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -276,7 +276,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_create_with_inexistent_foreign_key_failing firm = Firm.create(name: 'GlobalMegaCorp') - assert_raises(ActiveRecord::UnknownAttributeError) do + assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) do firm.create_account_with_inexistent_foreign_key end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ea2b94cbf4..243c90e945 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -758,12 +758,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_bulk_update_respects_access_control privatize("title=(value)") - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") } - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } + assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new(:title => "Rants about pants") } + assert_raise(ActiveModel::AttributeAssignment::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end def test_bulk_update_raise_unknown_attribute_error - error = assert_raises(ActiveRecord::UnknownAttributeError) { + error = assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) { Topic.new(hello: "world") } assert_instance_of Topic, error.record diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index dbe1eb48db..4ddf6e7ba0 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -58,7 +58,7 @@ module ActiveRecord data = OverloadedType.new(non_existent_decimal: 1) assert_equal BigDecimal.new(1), data.non_existent_decimal - assert_raise ActiveRecord::UnknownAttributeError do + assert_raise ActiveModel::AttributeAssignment::UnknownAttributeError do UnoverloadedType.new(non_existent_decimal: 1) end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 5c7e8a65d2..8bc42b8890 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -672,7 +672,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveRecord::UnknownAttributeError do + assert_nothing_raised ActiveModel::AttributeAssignment::UnknownAttributeError do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end |