aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib/active_model')
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb18
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/dirty.rb8
-rw-r--r--activemodel/lib/active_model/errors.rb10
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb5
-rw-r--r--activemodel/lib/active_model/naming.rb6
-rw-r--r--activemodel/lib/active_model/serialization.rb45
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb238
-rw-r--r--activemodel/lib/active_model/type.rb59
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb13
-rw-r--r--activemodel/lib/active_model/type/binary.rb50
-rw-r--r--activemodel/lib/active_model/type/boolean.rb21
-rw-r--r--activemodel/lib/active_model/type/date.rb50
-rw-r--r--activemodel/lib/active_model/type/date_time.rb44
-rw-r--r--activemodel/lib/active_model/type/decimal.rb52
-rw-r--r--activemodel/lib/active_model/type/decimal_without_scale.rb11
-rw-r--r--activemodel/lib/active_model/type/float.rb25
-rw-r--r--activemodel/lib/active_model/type/helpers.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb35
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb18
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb34
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb77
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb29
-rw-r--r--activemodel/lib/active_model/type/integer.rb66
-rw-r--r--activemodel/lib/active_model/type/registry.rb64
-rw-r--r--activemodel/lib/active_model/type/string.rb19
-rw-r--r--activemodel/lib/active_model/type/text.rb11
-rw-r--r--activemodel/lib/active_model/type/time.rb42
-rw-r--r--activemodel/lib/active_model/type/unsigned_integer.rb15
-rw-r--r--activemodel/lib/active_model/type/value.rb107
-rw-r--r--activemodel/lib/active_model/validations.rb10
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb62
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb3
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb20
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/helper_methods.rb13
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb6
-rw-r--r--activemodel/lib/active_model/validations/length.rb15
-rw-r--r--activemodel/lib/active_model/validations/validates.rb2
-rw-r--r--activemodel/lib/active_model/validations/with.rb10
40 files changed, 1014 insertions, 308 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index ff7280f9e5..1963a3fc4e 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'mutex_m'
module ActiveModel
@@ -225,7 +225,7 @@ module ActiveModel
end
# Declares the attributes that should be prefixed and suffixed by
- # ActiveModel::AttributeMethods.
+ # <tt>ActiveModel::AttributeMethods</tt>.
#
# To use, pass attribute names (as strings or symbols). Be sure to declare
# +define_attribute_methods+ after you define any prefix, suffix or affix
@@ -253,7 +253,7 @@ module ActiveModel
end
# Declares an attribute that should be prefixed and suffixed by
- # ActiveModel::AttributeMethods.
+ # <tt>ActiveModel::AttributeMethods</tt>.
#
# To use, pass an attribute name (as string or symbol). Be sure to declare
# +define_attribute_method+ after you define any prefix, suffix or affix
@@ -342,7 +342,7 @@ module ActiveModel
private
# The methods +method_missing+ and +respond_to?+ of this module are
# invoked often in a typical rails, both of which invoke the method
- # +match_attribute_method?+. The latter method iterates through an
+ # +matched_attribute_method+. The latter method iterates through an
# array doing regular expression matches, which results in a lot of
# object creations. Most of the time it returns a +nil+ match. As the
# match result is always the same given a +method_name+, this cache is
@@ -350,7 +350,7 @@ module ActiveModel
# significantly (in our case our test suite finishes 10% faster with
# this cache).
def attribute_method_matchers_cache #:nodoc:
- @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
def attribute_method_matchers_matching(method_name) #:nodoc:
@@ -372,7 +372,7 @@ module ActiveModel
"define_method(:'#{name}') do |*args|"
end
- extra = (extra.map!(&:inspect) << "*args").join(", ")
+ extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
target = if send =~ CALL_COMPILABLE_REGEXP
"#{"self." unless include_private}#{send}(#{extra})"
@@ -429,7 +429,7 @@ module ActiveModel
if respond_to_without_attributes?(method, true)
super
else
- match = match_attribute_method?(method.to_s)
+ match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
@@ -454,7 +454,7 @@ module ActiveModel
# but found among all methods. Which means that the given method is private.
false
else
- !match_attribute_method?(method.to_s).nil?
+ !matched_attribute_method(method.to_s).nil?
end
end
@@ -466,7 +466,7 @@ module ActiveModel
private
# Returns a struct representing the matching attribute method.
# The struct's attributes are prefix, base and suffix.
- def match_attribute_method?(method_name)
+ def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 2cf39b68fb..0d6a3dc52d 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -103,6 +103,7 @@ module ActiveModel
def define_model_callbacks(*callbacks)
options = callbacks.extract_options!
options = {
+ terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name],
only: [:before, :around, :after]
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index c0fc507286..0ab8df42f5 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -102,10 +102,10 @@ module ActiveModel
# person.changes # => {"name" => ["Bill", "Bob"]}
#
# If an attribute is modified in-place then make use of
- # +[attribute_name]_will_change!+ to mark that the attribute is changing.
+ # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
# Otherwise \Active \Model can't track changes to in-place attributes. Note
# that Active Record can detect in-place modifications automatically. You do
- # not need to call +[attribute_name]_will_change!+ on Active Record models.
+ # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
#
# person.name_will_change!
# person.name_change # => ["Bill", "Bill"]
@@ -203,7 +203,7 @@ module ActiveModel
# Returns +true+ if attr_name were changed before the model was saved,
# +false+ otherwise.
def previous_changes_include?(attr_name)
- @previously_changed.include?(attr_name)
+ previous_changes.include?(attr_name)
end
# Removes current changes and makes them accessible through +previous_changes+.
@@ -225,7 +225,7 @@ module ActiveModel
# Handles <tt>*_previous_change</tt> for +method_missing+.
def attribute_previous_change(attr)
- @previously_changed[attr] if attribute_previously_changed?(attr)
+ previous_changes[attr] if attribute_previously_changed?(attr)
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index f843b279ce..4726a68f69 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/object/deep_dup'
@@ -43,11 +41,11 @@ module ActiveModel
# end
# end
#
- # The last three methods are required in your object for Errors to be
+ # The last three methods are required in your object for +Errors+ to be
# able to generate error messages correctly and also handle multiple
- # languages. Of course, if you extend your object with ActiveModel::Translation
+ # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt>
# you will not need to implement the last two. Likewise, using
- # ActiveModel::Validations will handle the validation related methods
+ # <tt>ActiveModel::Validations</tt> will handle the validation related methods
# for you.
#
# The above allows you to do:
@@ -452,7 +450,6 @@ module ActiveModel
defaults = []
end
- defaults << options.delete(:message)
defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
@@ -461,6 +458,7 @@ module ActiveModel
defaults.flatten!
key = defaults.shift
+ defaults = options.delete(:message) if options[:message]
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
options = {
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index b4fa378601..d2c6a89cc2 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -17,8 +17,9 @@ module ActiveModel
module ForbiddenAttributesProtection # :nodoc:
protected
def sanitize_for_mass_assignment(attributes)
- if attributes.respond_to?(:permitted?) && !attributes.permitted?
- raise ActiveModel::ForbiddenAttributesError
+ if attributes.respond_to?(:permitted?)
+ raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
+ attributes.to_h
else
attributes
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 1f1749af4e..d86ef6224e 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -164,7 +164,7 @@ module ActiveModel
@route_key << "_index" if @plural == @singular
end
- # Transform the model name into a more humane format, using I18n. By default,
+ # Transform the model name into a more human format, using I18n. By default,
# it will underscore then humanize the class name.
#
# class BlogPost
@@ -192,7 +192,7 @@ module ActiveModel
private
def _singularize(string)
- ActiveSupport::Inflector.underscore(string).tr('/', '_')
+ ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze)
end
end
@@ -226,7 +226,7 @@ module ActiveModel
# (See ActiveModel::Name for more information).
#
# class Person
- # include ActiveModel::Model
+ # extend ActiveModel::Naming
# end
#
# Person.model_name.name # => "Person"
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 976f50b13e..70e10fa06d 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -31,16 +31,14 @@ module ActiveModel
# of the attributes hash's keys. In order to override this behavior, take a look
# at the private method +read_attribute_for_serialization+.
#
- # Most of the time though, either the JSON or XML serializations are needed.
- # Both of these modules automatically include the
- # <tt>ActiveModel::Serialization</tt> module, so there is no need to
- # explicitly include it.
+ # ActiveModel::Serializers::JSON module automatically includes
+ # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
+ # explicitly include <tt>ActiveModel::Serialization</tt>.
#
- # A minimal implementation including XML and JSON would be:
+ # A minimal implementation including JSON would be:
#
# class Person
# include ActiveModel::Serializers::JSON
- # include ActiveModel::Serializers::Xml
#
# attr_accessor :name
#
@@ -55,13 +53,11 @@ module ActiveModel
# person.serializable_hash # => {"name"=>nil}
# person.as_json # => {"name"=>nil}
# person.to_json # => "{\"name\":null}"
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
# person.as_json # => {"name"=>"Bob"}
# person.to_json # => "{\"name\":\"Bob\"}"
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
# <tt>:include</tt>. The following are all valid examples:
@@ -94,6 +90,37 @@ module ActiveModel
# person.serializable_hash(except: :name) # => {"age"=>22}
# person.serializable_hash(methods: :capitalized_name)
# # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
+ #
+ # Example with <tt>:include</tt> option
+ #
+ # class User
+ # include ActiveModel::Serializers::JSON
+ # attr_accessor :name, :notes # Emulate has_many :notes
+ # def attributes
+ # {'name' => nil}
+ # end
+ # end
+ #
+ # class Note
+ # include ActiveModel::Serializers::JSON
+ # attr_accessor :title, :text
+ # def attributes
+ # {'title' => nil, 'text' => nil}
+ # end
+ # end
+ #
+ # note = Note.new
+ # note.title = 'Battle of Austerlitz'
+ # note.text = 'Some text here'
+ #
+ # user = User.new
+ # user.name = 'Napoleon'
+ # user.notes = [note]
+ #
+ # user.serializable_hash
+ # # => {"name" => "Napoleon"}
+ # user.serializable_hash(include: { notes: { only: 'title' }})
+ # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
def serializable_hash(options = nil)
options ||= {}
@@ -107,7 +134,7 @@ module ActiveModel
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
- Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
serializable_add_includes(options) do |association, records, opts|
hash[association.to_s] = if records.respond_to?(:to_ary)
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
deleted file mode 100644
index e33c766627..0000000000
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/array/conversions'
-require 'active_support/core_ext/hash/conversions'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/time/acts_like'
-
-module ActiveModel
- module Serializers
- # == \Active \Model XML Serializer
- module Xml
- extend ActiveSupport::Concern
- include ActiveModel::Serialization
-
- included do
- extend ActiveModel::Naming
- end
-
- class Serializer #:nodoc:
- class Attribute #:nodoc:
- attr_reader :name, :value, :type
-
- def initialize(name, serializable, value)
- @name, @serializable = name, serializable
-
- if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
- value = value.in_time_zone
- end
-
- @value = value
- @type = compute_type
- end
-
- def decorations
- decorations = {}
- decorations[:encoding] = 'base64' if type == :binary
- decorations[:type] = (type == :string) ? nil : type
- decorations[:nil] = true if value.nil?
- decorations
- end
-
- protected
-
- def compute_type
- return if value.nil?
- type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
- type ||= :string if value.respond_to?(:to_str)
- type ||= :yaml
- type
- end
- end
-
- class MethodAttribute < Attribute #:nodoc:
- end
-
- attr_reader :options
-
- def initialize(serializable, options = nil)
- @serializable = serializable
- @options = options ? options.dup : {}
- end
-
- def serializable_hash
- @serializable.serializable_hash(@options.except(:include))
- end
-
- def serializable_collection
- methods = Array(options[:methods]).map(&:to_s)
- serializable_hash.map do |name, value|
- name = name.to_s
- if methods.include?(name)
- self.class::MethodAttribute.new(name, @serializable, value)
- else
- self.class::Attribute.new(name, @serializable, value)
- end
- end
- end
-
- def serialize
- require 'builder' unless defined? ::Builder
-
- options[:indent] ||= 2
- options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
-
- @builder = options[:builder]
- @builder.instruct! unless options[:skip_instruct]
-
- root = (options[:root] || @serializable.model_name.element).to_s
- root = ActiveSupport::XmlMini.rename_key(root, options)
-
- args = [root]
- args << { xmlns: options[:namespace] } if options[:namespace]
- args << { type: options[:type] } if options[:type] && !options[:skip_types]
-
- @builder.tag!(*args) do
- add_attributes_and_methods
- add_includes
- add_extra_behavior
- add_procs
- yield @builder if block_given?
- end
- end
-
- private
-
- def add_extra_behavior
- end
-
- def add_attributes_and_methods
- serializable_collection.each do |attribute|
- key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
- ActiveSupport::XmlMini.to_tag(key, attribute.value,
- options.merge(attribute.decorations))
- end
- end
-
- def add_includes
- @serializable.send(:serializable_add_includes, options) do |association, records, opts|
- add_associations(association, records, opts)
- end
- end
-
- # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
- def add_associations(association, records, opts)
- merged_options = opts.merge(options.slice(:builder, :indent))
- merged_options[:skip_instruct] = true
-
- [:skip_types, :dasherize, :camelize].each do |key|
- merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil?
- end
-
- if records.respond_to?(:to_ary)
- records = records.to_ary
-
- tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
- type = options[:skip_types] ? { } : { type: "array" }
- association_name = association.to_s.singularize
- merged_options[:root] = association_name
-
- if records.empty?
- @builder.tag!(tag, type)
- else
- @builder.tag!(tag, type) do
- records.each do |record|
- if options[:skip_types]
- record_type = {}
- else
- record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
- record_type = { type: record_class }
- end
-
- record.to_xml merged_options.merge(record_type)
- end
- end
- end
- else
- merged_options[:root] = association.to_s
-
- unless records.class.to_s.underscore == association.to_s
- merged_options[:type] = records.class.name
- end
-
- records.to_xml merged_options
- end
- end
-
- def add_procs
- if procs = options.delete(:procs)
- Array(procs).each do |proc|
- if proc.arity == 1
- proc.call(options)
- else
- proc.call(options, @serializable)
- end
- end
- end
- end
- end
-
- # Returns XML representing the model. Configuration can be
- # passed through +options+.
- #
- # Without any +options+, the returned XML string will include all the
- # model's attributes.
- #
- # user = User.find(1)
- # user.to_xml
- #
- # <?xml version="1.0" encoding="UTF-8"?>
- # <user>
- # <id type="integer">1</id>
- # <name>David</name>
- # <age type="integer">16</age>
- # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at>
- # </user>
- #
- # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
- # attributes included, and work similar to the +attributes+ method.
- #
- # To include the result of some method calls on the model use <tt>:methods</tt>.
- #
- # To include associations use <tt>:include</tt>.
- #
- # For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt>
- def to_xml(options = {}, &block)
- Serializer.new(self, options).serialize(&block)
- end
-
- # Sets the model +attributes+ from an XML string. Returns +self+.
- #
- # class Person
- # include ActiveModel::Serializers::Xml
- #
- # attr_accessor :name, :age, :awesome
- #
- # def attributes=(hash)
- # hash.each do |key, value|
- # instance_variable_set("@#{key}", value)
- # end
- # end
- #
- # def attributes
- # instance_values
- # end
- # end
- #
- # xml = { name: 'bob', age: 22, awesome:true }.to_xml
- # person = Person.new
- # person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob">
- # person.name # => "bob"
- # person.age # => 22
- # person.awesome # => true
- def from_xml(xml)
- self.attributes = Hash.from_xml(xml).values.first
- self
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
new file mode 100644
index 0000000000..bec851594f
--- /dev/null
+++ b/activemodel/lib/active_model/type.rb
@@ -0,0 +1,59 @@
+require 'active_model/type/helpers'
+require 'active_model/type/value'
+
+require 'active_model/type/big_integer'
+require 'active_model/type/binary'
+require 'active_model/type/boolean'
+require 'active_model/type/date'
+require 'active_model/type/date_time'
+require 'active_model/type/decimal'
+require 'active_model/type/decimal_without_scale'
+require 'active_model/type/float'
+require 'active_model/type/immutable_string'
+require 'active_model/type/integer'
+require 'active_model/type/string'
+require 'active_model/type/text'
+require 'active_model/type/time'
+require 'active_model/type/unsigned_integer'
+
+require 'active_model/type/registry'
+
+module ActiveModel
+ module Type
+ @registry = Registry.new
+
+ class << self
+ attr_accessor :registry # :nodoc:
+ delegate :add_modifier, to: :registry
+
+ # Add a new type to the registry, allowing it to be referenced as a
+ # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your
+ # type is only meant to be used with a specific database adapter, you can
+ # do so by passing +adapter: :postgresql+. If your type has the same
+ # name as a native type for the current adapter, an exception will be
+ # raised unless you specify an +:override+ option. +override: true+ will
+ # cause your type to be used instead of the native type. +override:
+ # false+ will cause the native type to be used over yours if one exists.
+ def register(type_name, klass = nil, **options, &block)
+ registry.register(type_name, klass, **options, &block)
+ end
+
+ def lookup(*args, **kwargs) # :nodoc:
+ registry.lookup(*args, **kwargs)
+ end
+ end
+
+ register(:big_integer, Type::BigInteger)
+ register(:binary, Type::Binary)
+ register(:boolean, Type::Boolean)
+ register(:date, Type::Date)
+ register(:date_time, Type::DateTime)
+ register(:decimal, Type::Decimal)
+ register(:float, Type::Float)
+ register(:immutable_string, Type::ImmutableString)
+ register(:integer, Type::Integer)
+ register(:string, Type::String)
+ register(:text, Type::Text)
+ register(:time, Type::Time)
+ end
+end
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
new file mode 100644
index 0000000000..4168cbfce7
--- /dev/null
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -0,0 +1,13 @@
+require 'active_model/type/integer'
+
+module ActiveModel
+ module Type
+ class BigInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ ::Float::INFINITY
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
new file mode 100644
index 0000000000..a0cc45b4c3
--- /dev/null
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -0,0 +1,50 @@
+module ActiveModel
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def serialize(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ old_value = deserialize(raw_old_value)
+ old_value != value
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+ alias_method :to_str, :to_s
+
+ def hex
+ @value.unpack('H*')[0]
+ end
+
+ def ==(other)
+ other == to_s || super
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
new file mode 100644
index 0000000000..c1bce98c87
--- /dev/null
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -0,0 +1,21 @@
+module ActiveModel
+ module Type
+ class Boolean < Value # :nodoc:
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
+
+ def type
+ :boolean
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ''
+ nil
+ else
+ !FALSE_VALUES.include?(value)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
new file mode 100644
index 0000000000..f74243a22c
--- /dev/null
+++ b/activemodel/lib/active_model/type/date.rb
@@ -0,0 +1,50 @@
+module ActiveModel
+ module Type
+ class Date < Value # :nodoc:
+ include Helpers::AcceptsMultiparameterTime.new
+
+ def type
+ :date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
+ def fast_string_to_date(string)
+ if string =~ ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+
+ def value_from_multiparameter_assignment(*)
+ time = super
+ time && time.to_date
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
new file mode 100644
index 0000000000..2f2df4320f
--- /dev/null
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -0,0 +1,44 @@
+module ActiveModel
+ module Type
+ class DateTime < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :datetime
+ end
+
+ private
+
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
+
+ fast_string_to_time(value) || fallback_string_to_time(value)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+
+ def value_from_multiparameter_assignment(values_hash)
+ missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
+ if missing_parameter
+ raise ArgumentError, missing_parameter
+ end
+ super
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
new file mode 100644
index 0000000000..d19d8baada
--- /dev/null
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -0,0 +1,52 @@
+require "bigdecimal/util"
+
+module ActiveModel
+ module Type
+ class Decimal < Value # :nodoc:
+ include Helpers::Numeric
+
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s.inspect
+ end
+
+ private
+
+ def cast_value(value)
+ casted_value = case value
+ when ::Float
+ convert_float_to_big_decimal(value)
+ when ::Numeric, ::String
+ BigDecimal(value, precision.to_i)
+ else
+ if value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+
+ scale ? casted_value.round(scale) : casted_value
+ end
+
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(value, float_precision)
+ else
+ value.to_d
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..129baa0c10
--- /dev/null
+++ b/activemodel/lib/active_model/type/decimal_without_scale.rb
@@ -0,0 +1,11 @@
+require 'active_model/type/big_integer'
+
+module ActiveModel
+ module Type
+ class DecimalWithoutScale < BigInteger # :nodoc:
+ def type
+ :decimal
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
new file mode 100644
index 0000000000..0f925bc7e1
--- /dev/null
+++ b/activemodel/lib/active_model/type/float.rb
@@ -0,0 +1,25 @@
+module ActiveModel
+ module Type
+ class Float < Value # :nodoc:
+ include Helpers::Numeric
+
+ def type
+ :float
+ end
+
+ alias serialize cast
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when "Infinity" then ::Float::INFINITY
+ when "-Infinity" then -::Float::INFINITY
+ when "NaN" then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
new file mode 100644
index 0000000000..a805a359ab
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -0,0 +1,4 @@
+require 'active_model/type/helpers/accepts_multiparameter_time'
+require 'active_model/type/helpers/numeric'
+require 'active_model/type/helpers/mutable'
+require 'active_model/type/helpers/time_value'
diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
new file mode 100644
index 0000000000..facea12704
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -0,0 +1,35 @@
+module ActiveModel
+ module Type
+ module Helpers
+ class AcceptsMultiparameterTime < Module # :nodoc:
+ def initialize(defaults: {})
+ define_method(:cast) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
+ define_method(:assert_valid_value) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
+ define_method(:value_from_multiparameter_assignment) do |values_hash|
+ defaults.each do |k, v|
+ values_hash[k] ||= v
+ end
+ return unless values_hash[1] && values_hash[2] && values_hash[3]
+ values = values_hash.sort.map(&:last)
+ ::Time.send(default_timezone, *values)
+ end
+ private :value_from_multiparameter_assignment
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
new file mode 100644
index 0000000000..4dddbe4e5e
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -0,0 +1,18 @@
+module ActiveModel
+ module Type
+ module Helpers
+ module Mutable # :nodoc:
+ def cast(value)
+ deserialize(serialize(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != serialize(new_value)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
new file mode 100644
index 0000000000..c883010506
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -0,0 +1,34 @@
+module ActiveModel
+ module Type
+ module Helpers
+ module Numeric # :nodoc:
+ def cast(value)
+ value = case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ value.to_s !~ /\A-?\d+\.?\d*\z/
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
new file mode 100644
index 0000000000..63993c0d93
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -0,0 +1,77 @@
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
+ module Type
+ module Helpers
+ module TimeValue # :nodoc:
+ def serialize(value)
+ value = apply_seconds_precision(value)
+
+ if value.acts_like?(:time)
+ zone_conversion_method = is_utc? ? :getutc : :getlocal
+
+ if value.respond_to?(zone_conversion_method)
+ value = value.send(zone_conversion_method)
+ end
+ end
+
+ value
+ end
+
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC'
+ end
+
+ def default_timezone
+ if is_utc?
+ :utc
+ else
+ :local
+ end
+ end
+
+ def apply_seconds_precision(value)
+ return value unless precision && value.respond_to?(:usec)
+ number_of_insignificant_digits = 6 - precision
+ round_power = 10 ** number_of_insignificant_digits
+ value.change(usec: value.usec / round_power * round_power)
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ def user_input_in_time_zone(value)
+ value.in_time_zone
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ is_utc? ? time : time.getlocal
+ else
+ ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb
new file mode 100644
index 0000000000..20b8ca0cc4
--- /dev/null
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -0,0 +1,29 @@
+module ActiveModel
+ module Type
+ class ImmutableString < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def serialize(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ result = case value
+ when true then "t"
+ when false then "f"
+ else value.to_s
+ end
+ result.freeze
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
new file mode 100644
index 0000000000..2f73ede009
--- /dev/null
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -0,0 +1,66 @@
+module ActiveModel
+ module Type
+ class Integer < Value # :nodoc:
+ include Helpers::Numeric
+
+ # Column storage size in bytes.
+ # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
+ DEFAULT_LIMIT = 4
+
+ def initialize(*)
+ super
+ @range = min_value...max_value
+ end
+
+ def type
+ :integer
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ value.to_i
+ end
+
+ def serialize(value)
+ result = cast(value)
+ if result
+ ensure_in_range(result)
+ end
+ result
+ end
+
+ protected
+
+ attr_reader :range
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else
+ value.to_i rescue nil
+ end
+ end
+
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
+ end
+ end
+
+ def max_value
+ 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
+ end
+
+ def min_value
+ -max_value
+ end
+
+ def _limit
+ self.limit || DEFAULT_LIMIT
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
new file mode 100644
index 0000000000..adc88eb624
--- /dev/null
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -0,0 +1,64 @@
+module ActiveModel
+ # :stopdoc:
+ module Type
+ class Registry
+ def initialize
+ @registrations = []
+ end
+
+ def register(type_name, klass = nil, **options, &block)
+ block ||= proc { |_, *args| klass.new(*args) }
+ registrations << registration_klass.new(type_name, block, **options)
+ end
+
+ def lookup(symbol, *args)
+ registration = find_registration(symbol, *args)
+
+ if registration
+ registration.call(self, symbol, *args)
+ else
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
+ end
+ end
+
+ protected
+
+ attr_reader :registrations
+
+ private
+
+ def registration_klass
+ Registration
+ end
+
+ def find_registration(symbol, *args)
+ registrations.find { |r| r.matches?(symbol, *args) }
+ end
+ end
+
+ class Registration
+ # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
+ def initialize(name, block, **)
+ @name = name
+ @block = block
+ end
+
+ def call(_registry, *args, **kwargs)
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
+ block.call(*args, **kwargs)
+ else
+ block.call(*args)
+ end
+ end
+
+ def matches?(type_name, *args, **kwargs)
+ type_name == name
+ end
+
+ protected
+
+ attr_reader :name, :block
+ end
+ end
+ # :startdoc:
+end
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
new file mode 100644
index 0000000000..8a91410998
--- /dev/null
+++ b/activemodel/lib/active_model/type/string.rb
@@ -0,0 +1,19 @@
+require "active_model/type/immutable_string"
+
+module ActiveModel
+ module Type
+ class String < ImmutableString # :nodoc:
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ ::String.new(super)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/text.rb b/activemodel/lib/active_model/type/text.rb
new file mode 100644
index 0000000000..1ad04daba4
--- /dev/null
+++ b/activemodel/lib/active_model/type/text.rb
@@ -0,0 +1,11 @@
+require 'active_model/type/string'
+
+module ActiveModel
+ module Type
+ class Text < String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
new file mode 100644
index 0000000000..7101bad566
--- /dev/null
+++ b/activemodel/lib/active_model/type/time.rb
@@ -0,0 +1,42 @@
+module ActiveModel
+ module Type
+ class Time < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :time
+ end
+
+ def user_input_in_time_zone(value)
+ return unless value.present?
+
+ case value
+ when ::String
+ value = "2000-01-01 #{value}"
+ when ::Time
+ value = value.change(year: 2000, day: 1, month: 1)
+ end
+
+ super(value)
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb
new file mode 100644
index 0000000000..3f49f9f5f7
--- /dev/null
+++ b/activemodel/lib/active_model/type/unsigned_integer.rb
@@ -0,0 +1,15 @@
+module ActiveModel
+ module Type
+ class UnsignedInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ super * 2
+ end
+
+ def min_value
+ 0
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
new file mode 100644
index 0000000000..5fea0561a6
--- /dev/null
+++ b/activemodel/lib/active_model/type/value.rb
@@ -0,0 +1,107 @@
+module ActiveModel
+ module Type
+ class Value
+ attr_reader :precision, :scale, :limit
+
+ def initialize(precision: nil, limit: nil, scale: nil)
+ @precision = precision
+ @scale = scale
+ @limit = limit
+ end
+
+ def type # :nodoc:
+ end
+
+ # Converts a value from database input to the appropriate ruby type. The
+ # return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. The default
+ # implementation just calls Value#cast.
+ #
+ # +value+ The raw input, as provided from the database.
+ def deserialize(value)
+ cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or a ruby object passed to a setter.
+ # There is currently no way to differentiate between which source it came
+ # from.
+ #
+ # The return value of this method will be returned from
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
+ # Value#cast_value.
+ #
+ # +value+ The raw input, as provided to the attribute setter.
+ def cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Casts a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+.
+ def serialize(value)
+ value
+ end
+
+ # Type casts a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def binary? # :nodoc:
+ false
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. If your type returns an object
+ # which could be mutated, you should override this method. You will need
+ # to either:
+ #
+ # - pass +new_value+ to Value#serialize and compare it to
+ # +raw_old_value+
+ #
+ # or
+ #
+ # - pass +raw_old_value+ to Value#deserialize and compare it to
+ # +new_value+
+ #
+ # +raw_old_value+ The original value, before being passed to
+ # +deserialize+.
+ #
+ # +new_value+ The current value, after type casting.
+ def changed_in_place?(raw_old_value, new_value)
+ false
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+
+ def assert_valid_value(*)
+ end
+
+ private
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by Value#cast for
+ # values except +nil+.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index c1019169e1..f23c920d87 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -87,8 +87,7 @@ module ActiveModel
validates_with BlockValidator, _merge_attributes(attr_names), &block
end
- # :nodoc:
- VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
# Adds a validation method or block to the class. This is useful when
# overriding the +validate+ instance method becomes too unwieldy and
@@ -130,6 +129,9 @@ module ActiveModel
# end
# end
#
+ # Note that the return value of validation methods is not relevant.
+ # It's not possible to halt the validate callback chain.
+ #
# Options:
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
# Runs in all validation contexts by default (nil). You can pass a symbol
@@ -160,7 +162,7 @@ module ActiveModel
options = options.dup
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- Array(options[:on]).include?(o.validation_context)
+ !(Array(options[:on]) & Array(o.validation_context)).empty?
}
end
@@ -402,7 +404,7 @@ module ActiveModel
protected
def run_validations! #:nodoc:
- run_callbacks :validate
+ _run_validate_callbacks
errors.empty?
end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index ee160fb483..c5c0cd4636 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -14,16 +14,63 @@ module ActiveModel
end
private
+
def setup!(klass)
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
- klass.send(:attr_reader, *attr_readers)
- klass.send(:attr_writer, *attr_writers)
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
end
def acceptable_option?(value)
Array(options[:accept]).include?(value)
end
+
+ class LazilyDefineAttributes < Module
+ def initialize(attribute_definition)
+ define_method(:respond_to_missing?) do |method_name, include_private=false|
+ super(method_name, include_private) || attribute_definition.matches?(method_name)
+ end
+
+ define_method(:method_missing) do |method_name, *args, &block|
+ if attribute_definition.matches?(method_name)
+ attribute_definition.define_on(self.class)
+ send(method_name, *args, &block)
+ else
+ super(method_name, *args, &block)
+ end
+ end
+ end
+ end
+
+ class AttributeDefinition
+ def initialize(attributes)
+ @attributes = attributes.map(&:to_s)
+ end
+
+ def matches?(method_name)
+ attr_name = convert_to_reader_name(method_name)
+ attributes.include?(attr_name)
+ end
+
+ def define_on(klass)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.send(:attr_reader, *attr_readers)
+ klass.send(:attr_writer, *attr_writers)
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def convert_to_reader_name(method_name)
+ attr_name = method_name.to_s
+ if attr_name.end_with?("=")
+ attr_name = attr_name[0..-2]
+ end
+ attr_name
+ end
+ end
end
module HelperMethods
@@ -42,9 +89,10 @@ module ActiveModel
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "must be
# accepted").
- # * <tt>:accept</tt> - Specifies value that is considered accepted.
- # The default value is a string "1", which makes it easy to relate to
- # an HTML checkbox. This should be set to +true+ if you are validating
+ # * <tt>:accept</tt> - Specifies a value that is considered accepted.
+ # Also accepts an array of possible values. The default value is
+ # an array ["1", true], which makes it easy to relate to an HTML
+ # checkbox. This should be set to, or include, +true+ if you are validating
# a database column, since the attribute is typecast from "1" to +true+
# before validation.
#
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index b4301c23e4..52111e5442 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -23,6 +23,7 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
+ terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
end
@@ -109,7 +110,7 @@ module ActiveModel
# Overwrite run validations to include callbacks.
def run_validations! #:nodoc:
- run_callbacks(:validation) { super }
+ _run_validation_callbacks { super }
end
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 1b11c28087..8f8ade90bb 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -3,14 +3,16 @@ module ActiveModel
module Validations
class ConfirmationValidator < EachValidator # :nodoc:
def initialize(options)
- super
+ super({ case_sensitive: true }.merge!(options))
setup!(options[:class])
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
- human_attribute_name = record.class.human_attribute_name(attribute)
- record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
+ if (confirmed = record.send("#{attribute}_confirmation"))
+ unless confirmation_value_equal?(record, attribute, value, confirmed)
+ human_attribute_name = record.class.human_attribute_name(attribute)
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
+ end
end
end
@@ -24,6 +26,14 @@ module ActiveModel
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
end.compact)
end
+
+ def confirmation_value_equal?(record, attribute, value, confirmed)
+ if !options[:case_sensitive] && value.is_a?(String)
+ value.casecmp(confirmed) == 0
+ else
+ value == confirmed
+ end
+ end
end
module HelperMethods
@@ -55,6 +65,8 @@ module ActiveModel
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "doesn't match
# <tt>%{translated_attribute_name}</tt>").
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
+ # non-text columns (+true+ by default).
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index f342d27275..6f4276cc2a 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -29,7 +29,9 @@ module ActiveModel
# Configuration options:
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't
# be part of. This can be supplied as a proc, lambda or symbol which returns an
- # enumerable. If the enumerable is a range the test is performed with
+ # enumerable. If the enumerable is a numerical, time or datetime range the test
+ # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When
+ # using a proc or lambda the instance under validation is passed as an argument.
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
# <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb
new file mode 100644
index 0000000000..2176115334
--- /dev/null
+++ b/activemodel/lib/active_model/validations/helper_methods.rb
@@ -0,0 +1,13 @@
+module ActiveModel
+ module Validations
+ module HelperMethods # :nodoc:
+ private
+ def _merge_attributes(attr_names)
+ options = attr_names.extract_options!.symbolize_keys
+ attr_names.flatten!
+ options[:attributes] = attr_names
+ options
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index c84025f083..03e0ef56d8 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -28,9 +28,9 @@ module ActiveModel
# Configuration options:
# * <tt>:in</tt> - An enumerable object of available items. This can be
# supplied as a proc, lambda or symbol which returns an enumerable. If the
- # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
- # otherwise with <tt>include?</tt>. When using a proc or lambda the instance
- # under validation is passed as an argument.
+ # enumerable is a numerical, time or datetime range the test is performed
+ # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using
+ # a proc or lambda the instance under validation is passed as an argument.
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
# not included in the list").
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index c22a58f9e1..910cca2f49 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,8 +1,6 @@
require "active_support/core_ext/string/strip"
module ActiveModel
-
- # == Active \Model Length Validator
module Validations
class LengthValidator < EachValidator # :nodoc:
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
@@ -100,8 +98,9 @@ module ActiveModel
module HelperMethods
- # Validates that the specified attribute matches the length restrictions
- # supplied. Only one option can be used at a time:
+ # Validates that the specified attributes match the length restrictions
+ # supplied. Only one constraint option can be used at a time apart from
+ # +:minimum+ and +:maximum+ that can be combined together:
#
# class Person < ActiveRecord::Base
# validates_length_of :first_name, maximum: 30
@@ -120,14 +119,18 @@ module ActiveModel
# end
# end
#
- # Configuration options:
+ # Constraint options:
+ #
# * <tt>:minimum</tt> - The minimum size of the attribute.
# * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
- # default if not used with :minimum.
+ # default if not used with +:minimum+.
# * <tt>:is</tt> - The exact size of the attribute.
# * <tt>:within</tt> - A range specifying the minimum and maximum size of
# the attribute.
# * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
+ #
+ # Other options:
+ #
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
# * <tt>:too_long</tt> - The error message if the attribute goes over the
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index bda436d8d0..1da4df21e7 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -115,7 +115,7 @@ module ActiveModel
key = "#{key.to_s.camelize}Validator"
begin
- validator = key.include?('::') ? key.constantize : const_get(key)
+ validator = key.include?('::'.freeze) ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index a2531327bf..6de01b3392 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -1,15 +1,5 @@
module ActiveModel
module Validations
- module HelperMethods
- private
- def _merge_attributes(attr_names)
- options = attr_names.extract_options!.symbolize_keys
- attr_names.flatten!
- options[:attributes] = attr_names
- options
- end
- end
-
class WithValidator < EachValidator # :nodoc:
def validate_each(record, attr, val)
method_name = options[:with]