aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb63
-rw-r--r--activemodel/lib/active_model/dirty.rb1
-rw-r--r--activemodel/lib/active_model/errors.rb58
-rw-r--r--activemodel/lib/active_model/model.rb7
-rw-r--r--activemodel/lib/active_model/naming.rb1
6 files changed, 107 insertions, 24 deletions
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..356421476c
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -0,0 +1,63 @@
+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)
+ if respond_to?("#{k}=")
+ public_send("#{k}=", v)
+ 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/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index afba9bab0d..614bc6a5d8 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -189,6 +189,7 @@ module ActiveModel
def changes_include?(attr_name)
attributes_changed_by_setter.include?(attr_name)
end
+ alias attribute_changed_by_setter? changes_include?
# Removes current changes and makes them accessible through +previous_changes+.
def changes_applied # :doc:
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 477edbd120..a809c72ccd 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -2,6 +2,7 @@
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/object/deep_dup'
module ActiveModel
# == Active \Model \Errors
@@ -23,7 +24,7 @@ module ActiveModel
# attr_reader :errors
#
# def validate!
- # errors.add(:name, "cannot be nil") if name.nil?
+ # errors.add(:name, :blank, message: "cannot be nil") if name.nil?
# end
#
# # The following methods are needed to be minimally implemented
@@ -58,8 +59,9 @@ module ActiveModel
include Enumerable
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
+ MESSAGE_OPTIONS = [:message]
- attr_reader :messages
+ attr_reader :messages, :details
# Pass in the instance of the object that is using the errors object.
#
@@ -71,10 +73,12 @@ module ActiveModel
def initialize(base)
@base = base
@messages = {}
+ @details = Hash.new { |details, attribute| details[attribute] = [] }
end
def initialize_dup(other) # :nodoc:
@messages = other.messages.dup
+ @details = other.details.deep_dup
super
end
@@ -85,6 +89,7 @@ module ActiveModel
# person.errors.full_messages # => []
def clear
messages.clear
+ details.clear
end
# Returns +true+ if the error messages include an error for the given key
@@ -126,6 +131,7 @@ module ActiveModel
# person.errors.get(:name) # => nil
def delete(key)
messages.delete(key)
+ details.delete(key)
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -149,12 +155,12 @@ module ActiveModel
# Yields the attribute and the error for that attribute. If the attribute
# has more than one error message, yields once for each error message.
#
- # person.errors.add(:name, "can't be blank")
+ # person.errors.add(:name, :blank, message: "can't be blank")
# person.errors.each do |attribute, error|
# # Will yield :name and "can't be blank"
# end
#
- # person.errors.add(:name, "must be specified")
+ # person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.each do |attribute, error|
# # Will yield :name and "can't be blank"
# # then yield :name and "must be specified"
@@ -167,9 +173,9 @@ module ActiveModel
# Returns the number of error messages.
#
- # person.errors.add(:name, "can't be blank")
+ # person.errors.add(:name, :blank, message: "can't be blank")
# person.errors.size # => 1
- # person.errors.add(:name, "must be specified")
+ # person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.size # => 2
def size
values.flatten.size
@@ -193,8 +199,8 @@ module ActiveModel
# Returns an array of error messages, with the attribute name included.
#
- # person.errors.add(:name, "can't be blank")
- # person.errors.add(:name, "must be specified")
+ # person.errors.add(:name, :blank, message: "can't be blank")
+ # person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.to_a # => ["name can't be blank", "name must be specified"]
def to_a
full_messages
@@ -202,9 +208,9 @@ module ActiveModel
# Returns the number of error messages.
#
- # person.errors.add(:name, "can't be blank")
+ # person.errors.add(:name, :blank, message: "can't be blank")
# person.errors.count # => 1
- # person.errors.add(:name, "must be specified")
+ # person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.count # => 2
def count
to_a.size
@@ -223,8 +229,8 @@ module ActiveModel
# Returns an xml formatted representation of the Errors hash.
#
- # person.errors.add(:name, "can't be blank")
- # person.errors.add(:name, "must be specified")
+ # person.errors.add(:name, :blank, message: "can't be blank")
+ # person.errors.add(:name, :not_specified, message: "must be specified")
# person.errors.to_xml
# # =>
# # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
@@ -261,17 +267,20 @@ module ActiveModel
end
end
- # Adds +message+ to the error messages on +attribute+. More than one error
- # can be added to the same +attribute+. If no +message+ is supplied,
- # <tt>:invalid</tt> is assumed.
+ # Adds +message+ to the error messages and used validator type to +details+ on +attribute+.
+ # More than one error can be added to the same +attribute+.
+ # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
#
# person.errors.add(:name)
# # => ["is invalid"]
- # person.errors.add(:name, 'must be implemented')
+ # person.errors.add(:name, :not_implemented, message: "must be implemented")
# # => ["is invalid", "must be implemented"]
#
# person.errors.messages
- # # => {:name=>["must be implemented", "is invalid"]}
+ # # => {:name=>["is invalid", "must be implemented"]}
+ #
+ # person.errors.details
+ # # => {:name=>[{error: :not_implemented}, {error: :invalid}]}
#
# If +message+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
@@ -293,16 +302,22 @@ module ActiveModel
# +attribute+ should be set to <tt>:base</tt> if the error is not
# directly associated with a single attribute.
#
- # person.errors.add(:base, "either name or email must be present")
+ # person.errors.add(:base, :name_or_email_blank,
+ # message: "either name or email must be present")
# person.errors.messages
# # => {:base=>["either name or email must be present"]}
+ # person.errors.details
+ # # => {:base=>[{error: :name_or_email_blank}]}
def add(attribute, message = :invalid, options = {})
+ message = message.call if message.respond_to?(:call)
+ detail = normalize_detail(attribute, message, options)
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
raise exception, full_message(attribute, message)
end
+ details[attribute.to_sym] << detail
self[attribute] << message
end
@@ -339,6 +354,7 @@ module ActiveModel
# person.errors.add :name, :blank
# person.errors.added? :name, :blank # => true
def added?(attribute, message = :invalid, options = {})
+ message = message.call if message.respond_to?(:call)
message = normalize_message(attribute, message, options)
self[attribute].include? message
end
@@ -447,12 +463,14 @@ module ActiveModel
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
- when Proc
- message.call
else
message
end
end
+
+ def normalize_detail(attribute, message, options)
+ { error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
+ end
end
# Raised when a validation cannot be corrected by end users and are considered
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index d51d6ddcc9..dac8d549a7 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -57,6 +57,7 @@ module ActiveModel
# (see below).
module Model
extend ActiveSupport::Concern
+ include ActiveModel::AttributeAssignment
include ActiveModel::Validations
include ActiveModel::Conversion
@@ -75,10 +76,8 @@ module ActiveModel
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => "18"
- def initialize(params={})
- params.each do |attr, value|
- self.public_send("#{attr}=", value)
- end if params
+ def initialize(attributes={})
+ assign_attributes(attributes) if attributes
super()
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index ada1f9a4f3..2bc3eeaa19 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/introspection'
+require 'active_support/core_ext/module/remove_method'
module ActiveModel
class Name