aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb4
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb2
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb32
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/conversion.rb4
-rw-r--r--activemodel/lib/active_model/dirty.rb45
-rw-r--r--activemodel/lib/active_model/errors.rb104
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb5
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb7
-rw-r--r--activemodel/lib/active_model/serialization.rb45
-rw-r--r--activemodel/lib/active_model/serializers/json.rb2
-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.rb60
-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.rb46
-rw-r--r--activemodel/lib/active_model/type/unsigned_integer.rb15
-rw-r--r--activemodel/lib/active_model/type/value.rb116
-rw-r--r--activemodel/lib/active_model/validations.rb10
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb58
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb7
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb7
-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.rb55
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb28
-rw-r--r--activemodel/lib/active_model/validations/validates.rb2
-rw-r--r--activemodel/lib/active_model/validations/with.rb10
-rw-r--r--activemodel/lib/active_model/validator.rb6
48 files changed, 1187 insertions, 397 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 8aa1b6f664..7de259a60d 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2016 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -49,6 +49,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :RangeError, 'active_model/errors'
autoload :StrictValidationFailed, 'active_model/errors'
autoload :UnknownAttributeError, 'active_model/errors'
end
@@ -58,7 +59,6 @@ module ActiveModel
eager_autoload do
autoload :JSON
- autoload :Xml
end
end
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index 087d11f708..62014cd1cd 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -27,7 +27,7 @@ module ActiveModel
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?
+ return if new_attributes.nil? || new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 96be551264..cc6285f932 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/map'
require 'mutex_m'
module ActiveModel
@@ -23,7 +23,7 @@ module ActiveModel
# The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
#
# * <tt>include ActiveModel::AttributeMethods</tt> in your class.
- # * Call each of its method you want to add, such as +attribute_method_suffix+
+ # * Call each of its methods you want to add, such as +attribute_method_suffix+
# or +attribute_method_prefix+.
# * Call +define_attribute_methods+ after the other methods are called.
# * Define the various generic +_attribute+ methods that you have declared.
@@ -225,9 +225,9 @@ 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
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
# +define_attribute_methods+ after you define any prefix, suffix or affix
# methods, or they will not hook in.
#
@@ -239,7 +239,7 @@ module ActiveModel
#
# # Call to define_attribute_methods must appear after the
# # attribute_method_prefix, attribute_method_suffix or
- # # attribute_method_affix declares.
+ # # attribute_method_affix declarations.
# define_attribute_methods :name, :age, :address
#
# private
@@ -253,9 +253,9 @@ 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
+ # 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
# method, or they will not hook in.
#
@@ -267,7 +267,7 @@ module ActiveModel
#
# # Call to define_attribute_method must appear after the
# # attribute_method_prefix, attribute_method_suffix or
- # # attribute_method_affix declares.
+ # # attribute_method_affix declarations.
# define_attribute_method :name
#
# private
@@ -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:
@@ -363,7 +363,7 @@ module ActiveModel
end
# Define a method `name` in `mod` that dispatches to `send`
- # using the given `extra` args. This fallbacks `define_method`
+ # using the given `extra` args. This falls back on `define_method`
# and `send` if the given names cannot be compiled.
def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc:
defn = if name =~ NAME_COMPILABLE_REGEXP
@@ -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})"
@@ -419,7 +419,7 @@ module ActiveModel
# returned by <tt>attributes</tt>, as though they were first-class
# methods. So a +Person+ class with a +name+ attribute can for example use
# <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
- # the attributes hash -- except for multiple assigns with
+ # the attributes hash -- except for multiple assignments with
# <tt>ActiveRecord::Base#attributes=</tt>.
#
# It's also possible to instantiate related objects, so a <tt>Client</tt>
@@ -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/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 9de6ea65be..a932ada45c 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -40,8 +40,8 @@ module ActiveModel
self
end
- # Returns an Array of all key attributes if any is set, regardless if
- # the object is persisted or not. Returns +nil+ if there are no key attributes.
+ # Returns an Array of all key attributes if any of the attributes is set, whether or not
+ # the object is persisted. Returns +nil+ if there are no key attributes.
#
# class Person
# include ActiveModel::Conversion
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index c03e5fac79..90047c3c12 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -26,6 +26,10 @@ module ActiveModel
#
# define_attribute_methods :name
#
+ # def initialize(name)
+ # @name = name
+ # end
+ #
# def name
# @name
# end
@@ -54,7 +58,7 @@ module ActiveModel
#
# A newly instantiated +Person+ object is unchanged:
#
- # person = Person.new
+ # person = Person.new("Uncle Bob")
# person.changed? # => false
#
# Change the name:
@@ -76,9 +80,11 @@ module ActiveModel
#
# Reset the changes:
#
- # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.name_previously_changed? # => true
+ # person.name_previous_change # => ["Uncle Bob", "Bill"]
# person.reload!
- # person.previous_changes # => {}
+ # person.previous_changes # => {}
#
# Rollback the changes:
#
@@ -100,10 +106,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"]
@@ -113,8 +119,12 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
+ OPTION_NOT_GIVEN = Object.new # :nodoc:
+ private_constant :OPTION_NOT_GIVEN
+
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
+ attribute_method_suffix '_previously_changed?', '_previous_change'
attribute_method_affix prefix: 'restore_', suffix: '!'
end
@@ -167,11 +177,10 @@ module ActiveModel
end
# Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, options = {}) #:nodoc:
- result = changes_include?(attr)
- result &&= options[:to] == __send__(attr) if options.key?(:to)
- result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
- result
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
+ !!changes_include?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == __send__(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
end
# Handles <tt>*_was</tt> for +method_missing+.
@@ -179,6 +188,11 @@ module ActiveModel
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
+ # Handles <tt>*_previously_changed?</tt> for +method_missing+.
+ def attribute_previously_changed?(attr) #:nodoc:
+ previous_changes_include?(attr)
+ end
+
# Restore all previous data of the provided attributes.
def restore_attributes(attributes = changed)
attributes.each { |attr| restore_attribute! attr }
@@ -192,6 +206,12 @@ module ActiveModel
end
alias attribute_changed_by_setter? changes_include?
+ # Returns +true+ if attr_name were changed before the model was saved,
+ # +false+ otherwise.
+ def previous_changes_include?(attr_name)
+ previous_changes.include?(attr_name)
+ end
+
# Removes current changes and makes them accessible through +previous_changes+.
def changes_applied # :doc:
@previously_changed = changes
@@ -209,6 +229,11 @@ module ActiveModel
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
+ # Handles <tt>*_previous_change</tt> for +method_missing+.
+ def attribute_previous_change(attr)
+ previous_changes[attr] if attribute_previously_changed?(attr)
+ end
+
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
return if attribute_changed?(attr)
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index f324788979..696e6d31da 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,8 +1,7 @@
-# -*- 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'
+require 'active_support/core_ext/string/filters'
module ActiveModel
# == Active \Model \Errors
@@ -33,20 +32,20 @@ module ActiveModel
# send(attr)
# end
#
- # def Person.human_attribute_name(attr, options = {})
+ # def self.human_attribute_name(attr, options = {})
# attr
# end
#
- # def Person.lookup_ancestors
+ # def self.lookup_ancestors
# [self]
# 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:
@@ -82,6 +81,18 @@ module ActiveModel
super
end
+ # Copies the errors from <tt>other</tt>.
+ #
+ # other - The ActiveModel::Errors instance.
+ #
+ # Examples
+ #
+ # person.errors.copy!(other)
+ def copy!(other) # :nodoc:
+ @messages = other.messages.dup
+ @details = other.details.dup
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
@@ -99,11 +110,9 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- messages[attribute].present?
+ messages.key?(attribute) && messages[attribute].present?
end
- # aliases include?
alias :has_key? :include?
- # aliases include?
alias :key? :include?
# Get messages for +key+.
@@ -123,9 +132,9 @@ module ActiveModel
# Set messages for +key+ to +value+.
#
- # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
# person.errors.set(:name, ["can't be nil"])
- # person.errors.get(:name) # => ["can't be nil"]
+ # person.errors[:name] # => ["can't be nil"]
def set(key, value)
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
ActiveModel::Errors#set is deprecated and will be removed in Rails 5.1.
@@ -138,12 +147,12 @@ module ActiveModel
# Delete messages for +key+. Returns the deleted messages.
#
- # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
# person.errors.delete(:name) # => ["cannot be nil"]
- # person.errors.get(:name) # => []
+ # person.errors[:name] # => []
def delete(key)
- messages.delete(key)
details.delete(key)
+ messages.delete(key)
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -151,6 +160,15 @@ module ActiveModel
#
# person.errors[:name] # => ["cannot be nil"]
# person.errors['name'] # => ["cannot be nil"]
+ #
+ # Note that, if you try to get errors of an attribute which has
+ # no errors associated with it, this method will instantiate
+ # an empty error list for it and +keys+ will return an array
+ # of error keys which includes this attribute.
+ #
+ # person.errors.keys # => []
+ # person.errors[:name] # => []
+ # person.errors.keys # => [:name]
def [](attribute)
messages[attribute.to_sym]
end
@@ -198,6 +216,7 @@ module ActiveModel
def size
values.flatten.size
end
+ alias :count :size
# Returns all message values.
#
@@ -215,35 +234,15 @@ module ActiveModel
messages.keys
end
- # Returns an array of error messages, with the attribute name included.
- #
- # 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
- end
-
- # Returns the number of error messages.
- #
- # person.errors.add(:name, :blank, message: "can't be blank")
- # person.errors.count # => 1
- # person.errors.add(:name, :not_specified, message: "must be specified")
- # person.errors.count # => 2
- def count
- to_a.size
- end
-
# Returns +true+ if no errors are found, +false+ otherwise.
# If the error message is a string it can be empty.
#
# person.errors.full_messages # => ["name cannot be nil"]
# person.errors.empty? # => false
def empty?
- all? { |k, v| v && v.empty? && !v.is_a?(String) }
+ size.zero?
end
- # aliases empty?
- alias_method :blank?, :empty?
+ alias :blank? :empty?
# Returns an xml formatted representation of the Errors hash.
#
@@ -311,9 +310,9 @@ module ActiveModel
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, :invalid, strict: true)
- # # => ActiveModel::StrictValidationFailed: name is invalid
+ # # => ActiveModel::StrictValidationFailed: Name is invalid
# person.errors.add(:name, :invalid, strict: NameIsInvalid)
- # # => NameIsInvalid: name is invalid
+ # # => NameIsInvalid: Name is invalid
#
# person.errors.messages # => {}
#
@@ -328,7 +327,7 @@ module ActiveModel
# # => {: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)
+ detail = normalize_detail(message, options)
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
@@ -347,7 +346,7 @@ module ActiveModel
# # => {:name=>["can't be empty"]}
def add_on_empty(attributes, options = {})
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1
+ ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1.
To achieve the same use:
@@ -369,7 +368,7 @@ module ActiveModel
# # => {:name=>["can't be blank"]}
def add_on_blank(attributes, options = {})
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1
+ ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1.
To achieve the same use:
@@ -406,6 +405,7 @@ module ActiveModel
def full_messages
map { |attribute, message| full_message(attribute, message) }
end
+ alias :to_a :full_messages
# Returns all the full error messages for a given attribute in an array.
#
@@ -471,7 +471,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}"
@@ -480,13 +479,15 @@ 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 = {
default: defaults,
model: @base.model_name.human,
attribute: @base.class.human_attribute_name(attribute),
- value: value
+ value: value,
+ object: @base
}.merge!(options)
I18n.translate(key, options)
@@ -502,7 +503,7 @@ module ActiveModel
end
end
- def normalize_detail(attribute, message, options)
+ def normalize_detail(message, options)
{ error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
end
end
@@ -525,7 +526,20 @@ module ActiveModel
class StrictValidationFailed < StandardError
end
+ # Raised when attribute values are out of range.
+ class RangeError < ::RangeError
+ end
+
# Raised when unknown attributes are supplied via mass assignment.
+ #
+ # class Person
+ # include ActiveModel::AttributeAssignment
+ # include ActiveModel::Validations
+ # end
+ #
+ # person = Person.new
+ # person.assign_attributes(name: 'Gorby')
+ # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person.
class UnknownAttributeError < NoMethodError
attr_reader :record, :attribute
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/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 762f4fe939..4a8ee915cf 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -6,7 +6,7 @@ module ActiveModel
module VERSION
MAJOR = 5
- MINOR = 0
+ MINOR = 1
TINY = 0
PRE = "alpha"
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 22010b517c..d86ef6224e 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/delegation'
module ActiveModel
class Name
@@ -163,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
@@ -191,7 +192,7 @@ module ActiveModel
private
def _singularize(string)
- ActiveSupport::Inflector.underscore(string).tr('/', '_')
+ ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze)
end
end
@@ -225,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/json.rb b/activemodel/lib/active_model/serializers/json.rb
index b66dbf1afe..b64a8299e6 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -10,7 +10,7 @@ module ActiveModel
included do
extend ActiveModel::Naming
- class_attribute :include_root_in_json
+ class_attribute :include_root_in_json, instance_writer: false
self.include_root_in_json = false
end
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..6ec3452478
--- /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(:datetime, 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..11ea327026
--- /dev/null
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -0,0 +1,60 @@
+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
+
+ apply_scale(casted_value)
+ end
+
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(apply_scale(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
+
+ def apply_scale(value)
+ if scale
+ value.round(scale)
+ else
+ value
+ 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..eea2280b22
--- /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 ActiveModel::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..34e09f0aba
--- /dev/null
+++ b/activemodel/lib/active_model/type/time.rb
@@ -0,0 +1,46 @@
+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?
+
+ if value =~ /^2000-01-01/
+ dummy_time_value = value
+ else
+ dummy_time_value = "2000-01-01 #{value}"
+ end
+
+ 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, :offset))
+ 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..0d2d6873a8
--- /dev/null
+++ b/activemodel/lib/active_model/type/value.rb
@@ -0,0 +1,116 @@
+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 map(value) # :nodoc:
+ yield value
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+ alias eql? ==
+
+ def hash
+ [self.class, precision, scale, limit].hash
+ 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 176d4c0607..8159b9b1d3 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -47,9 +47,10 @@ module ActiveModel
include HelperMethods
attr_accessor :validation_context
+ private :validation_context=
define_callbacks :validate, scope: :name
- class_attribute :_validators
+ class_attribute :_validators, instance_writer: false
self._validators = Hash.new { |h,k| h[k] = [] }
end
@@ -87,7 +88,7 @@ module ActiveModel
validates_with BlockValidator, _merge_attributes(attr_names), &block
end
- 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
@@ -129,6 +130,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
@@ -159,7 +163,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 ee160fb483..a04e5f347e 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -14,16 +14,59 @@ 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)
+ method_name.to_s.chomp('=')
+ end
+ end
end
module HelperMethods
@@ -42,9 +85,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 4b58ef66e3..a201f72ed0 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -23,13 +23,13 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
+ terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
end
module ClassMethods
- # Defines a callback that will get called right before validation
- # happens.
+ # Defines a callback that will get called right before validation.
#
# class Person
# include ActiveModel::Validations
@@ -64,8 +64,7 @@ module ActiveModel
set_callback(:validation, :before, *args, &block)
end
- # Defines a callback that will get called right after validation
- # happens.
+ # Defines a callback that will get called right after validation.
#
# class Person
# include ActiveModel::Validations
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index bad9e4f9a9..d49af603bb 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -30,14 +30,15 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
- # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
+ # endpoints, which is fast but is only accurate on Numeric, Time, Date,
+ # or DateTime ranges.
def inclusion_method(enumerable)
if enumerable.is_a? Range
case enumerable.first
- when Numeric, Time, DateTime
+ when Numeric, Time, DateTime, Date
:cover?
else
:include?
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 23201b264a..79297ac119 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,6 +1,6 @@
-module ActiveModel
+require "active_support/core_ext/string/strip"
- # == Active \Model Length Validator
+module ActiveModel
module Validations
class LengthValidator < EachValidator # :nodoc:
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
@@ -18,6 +18,27 @@ module ActiveModel
options[:minimum] = 1
end
+ if options[:tokenizer]
+ ActiveSupport::Deprecation.warn(<<-EOS.strip_heredoc)
+ The `:tokenizer` option is deprecated, and will be removed in Rails 5.1.
+ You can achieve the same functionality by defining an instance method
+ with the value that you want to validate the length of. For example,
+
+ validates_length_of :essay, minimum: 100,
+ tokenizer: ->(str) { str.scan(/\w+/) }
+
+ should be written as
+
+ validates_length_of :words_in_essay, minimum: 100
+
+ private
+
+ def words_in_essay
+ essay.scan(/\w+/)
+ end
+ EOS
+ end
+
super
end
@@ -77,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
@@ -88,36 +110,39 @@ module ActiveModel
# validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
# validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
# validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
- # validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.',
- # tokenizer: ->(str) { str.scan(/\w+/) }
+ # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.'
+ #
+ # private
+ #
+ # def words_in_essay
+ # essay.scan(/\w+/)
+ # 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
# maximum (default is: "is too long (maximum is %{count} characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the
- # minimum (default is: "is too short (min is %{count} characters)").
+ # minimum (default is: "is too short (minimum is %{count} characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
# method and the attribute is the wrong size (default is: "is the wrong
# length (should be %{count} characters)").
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
# <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
# <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
- # * <tt>:tokenizer</tt> - A method (as a symbol), proc or string to
- # specify how to split up the attribute string. (e.g.
- # <tt>tokenizer: :word_tokenizer</tt> to call the +word_tokenizer+ method
- # or <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words as in
- # above example). Defaults to <tt>->(value) { value.split(//) }</tt> which
- # counts individual characters.
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+ and +:strict+.
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 4ba4e3e8f7..ad7012df48 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -20,7 +20,7 @@ module ActiveModel
def validate_each(record, attr_name, value)
before_type_cast = :"#{attr_name}_before_type_cast"
- raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
+ raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value
raw_value ||= value
if record_attribute_changed_in_place?(record, attr_name)
@@ -29,16 +29,18 @@ module ActiveModel
return if options[:allow_nil] && raw_value.nil?
- unless value = parse_raw_value_as_a_number(raw_value)
+ unless is_number?(raw_value)
record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
return
end
- if allow_only_integer?(record)
- unless value = parse_raw_value_as_an_integer(raw_value)
- record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
- return
- end
+ if allow_only_integer?(record) && !is_integer?(raw_value)
+ record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
+ return
+ end
+
+ unless raw_value.is_a?(Numeric)
+ value = parse_raw_value_as_a_number(raw_value)
end
options.slice(*CHECKS.keys).each do |option, option_value|
@@ -64,14 +66,18 @@ module ActiveModel
protected
+ def is_number?(raw_value)
+ !parse_raw_value_as_a_number(raw_value).nil?
+ rescue ArgumentError, TypeError
+ false
+ end
+
def parse_raw_value_as_a_number(raw_value)
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
- rescue ArgumentError, TypeError
- nil
end
- def parse_raw_value_as_an_integer(raw_value)
- raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/
+ def is_integer?(raw_value)
+ /\A[+-]?\d+\z/ === raw_value.to_s
end
def filtered_options(value)
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]
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 1d2888a818..699f74ed17 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -100,7 +100,7 @@ module ActiveModel
# PresenceValidator.kind # => :presence
# UniquenessValidator.kind # => :uniqueness
def self.kind
- @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous?
+ @kind ||= name.split('::').last.underscore.chomp('_validator').to_sym unless anonymous?
end
# Accepts options that will be made available through the +options+ reader.
@@ -163,10 +163,6 @@ module ActiveModel
# +ArgumentError+ when invalid options are supplied.
def check_validity!
end
-
- def should_validate?(record) # :nodoc:
- !record.persisted? || record.changed? || record.marked_for_destruction?
- end
end
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization