From 2606fb339797a99c50e531105fc92071cef3db01 Mon Sep 17 00:00:00 2001 From: Bogdan Gusiev Date: Fri, 23 Jan 2015 13:57:34 +0200 Subject: Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributesAssignment` Allows to use it for any object as an includable module. --- activemodel/CHANGELOG.md | 20 ++++ activemodel/lib/active_model.rb | 1 + .../lib/active_model/attribute_assignment.rb | 64 +++++++++++++ .../test/cases/attribute_assignment_test.rb | 106 +++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 activemodel/lib/active_model/attribute_assignment.rb create mode 100644 activemodel/test/cases/attribute_assignment_test.rb (limited to 'activemodel') 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 permitted? method and the return value + # of this method is +false+ an ActiveModel::ForbiddenAttributesError + # 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 -- cgit v1.2.3 From a225d4bec51778d99ccba5f0d6700dd00d2474f4 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Fri, 23 Jan 2015 14:51:59 -0700 Subject: =?UTF-8?q?=E2=9C=82=EF=B8=8F=20and=20=F0=9F=92=85=20for=20#10776?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor style changes across the board. Changed an alias to an explicit method declaration, since the alias will not be documented otherwise. --- activemodel/CHANGELOG.md | 2 +- .../lib/active_model/attribute_assignment.rb | 5 +- .../test/cases/attribute_assignment_test.rb | 59 +++++++++++----------- 3 files changed, 33 insertions(+), 33 deletions(-) (limited to 'activemodel') diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index e20ebde8b1..9cebb680fc 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -6,7 +6,7 @@ include ActiveModel::AttributeAssignment attr_accessor :name, :status end - + cat = Cat.new cat.assign_attributes(name: "Gorby", status: "yawning") cat.name # => 'Gorby' diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index 36ddbad826..356421476c 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -34,6 +34,7 @@ module ActiveModel end private + def _assign_attributes(attributes) attributes.each do |k, v| _assign_attribute(k, v) @@ -41,10 +42,8 @@ module ActiveModel end def _assign_attribute(k, v) - public_send("#{k}=", v) - rescue NoMethodError if respond_to?("#{k}=") - raise + public_send("#{k}=", v) else raise UnknownAttributeError.new(self, k) end diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 5c1be6a456..402caf21f7 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -1,8 +1,7 @@ -require 'cases/helper' -require 'active_support/hash_with_indifferent_access' +require "cases/helper" +require "active_support/hash_with_indifferent_access" class AttributeAssignmentTest < ActiveModel::TestCase - class Model include ActiveModel::AttributeAssignment @@ -13,13 +12,15 @@ class AttributeAssignmentTest < ActiveModel::TestCase end def broken_attribute=(value) - non_existing_method(value) + raise ErrorFromAttributeWriter end - private - def metadata=(data) - @metadata = data - end + protected + + attr_writer :metadata + end + + class ErrorFromAttributeWriter < StandardError end class ProtectedParams < ActiveSupport::HashWithIndifferentAccess @@ -41,14 +42,14 @@ class AttributeAssignmentTest < ActiveModel::TestCase test "simple assignment" do model = Model.new - model.assign_attributes(name: 'hello', description: 'world') - assert_equal 'hello', model.name - assert_equal 'world', model.description + 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 + error = assert_raises(ActiveModel::AttributeAssignment::UnknownAttributeError) do model.assign_attributes(hz: 1) end @@ -58,49 +59,49 @@ class AttributeAssignmentTest < ActiveModel::TestCase test "assign private attribute" do model = Model.new - assert_raises ActiveModel::AttributeAssignment::UnknownAttributeError do + 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 + test "does not swallow errors raised in an attribute writer" do + assert_raises(ErrorFromAttributeWriter) do Model.new(broken_attribute: 1) end end - test "raises ArgumentError if non-hash object passed" do - assert_raises ArgumentError do + test "an ArgumentError is raised if a non-hash-like obejct is 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') + 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') + 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 + 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') + 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 + assert_equal "Guille", model.name + assert_equal "m", model.description end - test 'blank attributes should not raise' do + test "assigning no attributes should not raise, even if the hash is un-permitted" do model = Model.new assert_nil model.assign_attributes(ProtectedParams.new({})) end - end -- cgit v1.2.3