aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md20
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb64
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb106
4 files changed, 191 insertions, 0 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