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.rb4
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/dirty.rb4
-rw-r--r--activemodel/lib/active_model/errors.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activemodel/lib/active_model/type.rb57
-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/integer.rb66
-rw-r--r--activemodel/lib/active_model/type/registry.rb64
-rw-r--r--activemodel/lib/active_model/type/string.rb36
-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.rb2
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb55
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb20
30 files changed, 907 insertions, 16 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 77e4ce3afe..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
@@ -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:
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 0169c20e0b..0ab8df42f5 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -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 29e0c977ce..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'
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 213f2d5b6a..de5fb27467 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
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
new file mode 100644
index 0000000000..f8ca7d0512
--- /dev/null
+++ b/activemodel/lib/active_model/type.rb
@@ -0,0 +1,57 @@
+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/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(: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/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..fd1630c751
--- /dev/null
+++ b/activemodel/lib/active_model/type/string.rb
@@ -0,0 +1,36 @@
+module ActiveModel
+ module Type
+ class String < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def serialize(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "t"
+ when false then "f"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
+ end
+ 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 5f1dde4aa3..f23c920d87 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -162,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
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 1bcfedb35d..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
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 4b58ef66e3..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
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+.