aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel/lib')
-rw-r--r--activemodel/lib/active_model.rb27
-rw-r--r--activemodel/lib/active_model/attribute.rb247
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb51
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb35
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb86
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb119
-rw-r--r--activemodel/lib/active_model/attribute_set.rb114
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb124
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb40
-rw-r--r--activemodel/lib/active_model/attributes.rb99
-rw-r--r--activemodel/lib/active_model/callbacks.rb50
-rw-r--r--activemodel/lib/active_model/conversion.rb23
-rw-r--r--activemodel/lib/active_model/dirty.rb221
-rw-r--r--activemodel/lib/active_model/errors.rb297
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb4
-rw-r--r--activemodel/lib/active_model/gem_version.rb4
-rw-r--r--activemodel/lib/active_model/lint.rb27
-rw-r--r--activemodel/lib/active_model/model.rb9
-rw-r--r--activemodel/lib/active_model/naming.rb43
-rw-r--r--activemodel/lib/active_model/railtie.rb8
-rw-r--r--activemodel/lib/active_model/secure_password.rb113
-rw-r--r--activemodel/lib/active_model/serialization.rb8
-rw-r--r--activemodel/lib/active_model/serializers/json.rb28
-rw-r--r--activemodel/lib/active_model/test_case.rb4
-rw-r--r--activemodel/lib/active_model/translation.rb7
-rw-r--r--activemodel/lib/active_model/type.rb56
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb10
-rw-r--r--activemodel/lib/active_model/type/binary.rb4
-rw-r--r--activemodel/lib/active_model/type/boolean.rb35
-rw-r--r--activemodel/lib/active_model/type/date.rb58
-rw-r--r--activemodel/lib/active_model/type/date_time.rb48
-rw-r--r--activemodel/lib/active_model/type/decimal.rb72
-rw-r--r--activemodel/lib/active_model/type/decimal_without_scale.rb11
-rw-r--r--activemodel/lib/active_model/type/float.rb27
-rw-r--r--activemodel/lib/active_model/type/helpers.rb10
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb10
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb6
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb37
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb57
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb19
-rw-r--r--activemodel/lib/active_model/type/integer.rb49
-rw-r--r--activemodel/lib/active_model/type/registry.rb24
-rw-r--r--activemodel/lib/active_model/type/string.rb13
-rw-r--r--activemodel/lib/active_model/type/text.rb11
-rw-r--r--activemodel/lib/active_model/type/time.rb22
-rw-r--r--activemodel/lib/active_model/type/unsigned_integer.rb15
-rw-r--r--activemodel/lib/active_model/type/value.rb31
-rw-r--r--activemodel/lib/active_model/validations.rb36
-rw-r--r--activemodel/lib/active_model/validations/absence.rb4
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb86
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb37
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb23
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb35
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb5
-rw-r--r--activemodel/lib/active_model/validations/format.rb49
-rw-r--r--activemodel/lib/active_model/validations/helper_methods.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb7
-rw-r--r--activemodel/lib/active_model/validations/length.rb60
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb44
-rw-r--r--activemodel/lib/active_model/validations/presence.rb4
-rw-r--r--activemodel/lib/active_model/validations/validates.rb23
-rw-r--r--activemodel/lib/active_model/validations/with.rb8
-rw-r--r--activemodel/lib/active_model/validator.rb29
-rw-r--r--activemodel/lib/active_model/version.rb4
64 files changed, 1978 insertions, 891 deletions
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 4e1b3f7495..bc10d6b4b9 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2015 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,36 +23,39 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_model/version'
+require "active_support"
+require "active_support/rails"
+require "active_model/version"
module ActiveModel
extend ActiveSupport::Autoload
+ autoload :Attribute
+ autoload :Attributes
autoload :AttributeAssignment
autoload :AttributeMethods
- autoload :BlockValidator, 'active_model/validator'
+ autoload :BlockValidator, "active_model/validator"
autoload :Callbacks
autoload :Conversion
autoload :Dirty
- autoload :EachValidator, 'active_model/validator'
+ autoload :EachValidator, "active_model/validator"
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
- autoload :Name, 'active_model/naming'
+ autoload :Name, "active_model/naming"
autoload :Naming
autoload :SecurePassword
autoload :Serialization
- autoload :TestCase
autoload :Translation
+ autoload :Type
autoload :Validations
autoload :Validator
eager_autoload do
autoload :Errors
- autoload :StrictValidationFailed, 'active_model/errors'
- autoload :UnknownAttributeError, 'active_model/errors'
+ autoload :RangeError, "active_model/errors"
+ autoload :StrictValidationFailed, "active_model/errors"
+ autoload :UnknownAttributeError, "active_model/errors"
end
module Serializers
@@ -68,5 +73,5 @@ module ActiveModel
end
ActiveSupport.on_load(:i18n) do
- I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml'
+ I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__)
end
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb
new file mode 100644
index 0000000000..75f60d205e
--- /dev/null
+++ b/activemodel/lib/active_model/attribute.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/duplicable"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type, original_attribute = nil)
+ FromUser.new(name, value, type, original_attribute)
+ end
+
+ def with_cast_value(name, value, type)
+ WithCastValue.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ @original_attribute = original_attribute
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def original_value
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
+ end
+
+ def value_for_database
+ type.serialize(value)
+ end
+
+ def changed?
+ changed_from_assignment? || changed_in_place?
+ end
+
+ def changed_in_place?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
+ end
+
+ def forgetting_assignment
+ with_value_from_database(value_for_database)
+ end
+
+ def with_value_from_user(value)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, original_attribute || self)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def with_cast_value(value)
+ self.class.with_cast_value(name, value, type)
+ end
+
+ def with_type(type)
+ if changed_in_place?
+ with_value_from_user(value).with_type(type)
+ else
+ self.class.new(name, value_before_type_cast, type, original_attribute)
+ end
+ end
+
+ def type_cast(*)
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ def came_from_user?
+ false
+ end
+
+ def has_been_read?
+ defined?(@value)
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+ alias eql? ==
+
+ def hash
+ [self.class, name, value_before_type_cast, type].hash
+ end
+
+ def init_with(coder)
+ @name = coder["name"]
+ @value_before_type_cast = coder["value_before_type_cast"]
+ @type = coder["type"]
+ @original_attribute = coder["original_attribute"]
+ @value = coder["value"] if coder.map.key?("value")
+ end
+
+ def encode_with(coder)
+ coder["name"] = name
+ coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
+ coder["type"] = type if type
+ coder["original_attribute"] = original_attribute if original_attribute
+ coder["value"] = value if defined?(@value)
+ end
+
+ protected
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ private
+ attr_reader :original_attribute
+ alias :assigned? :original_attribute
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ def changed_from_assignment?
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
+ end
+
+ def _original_value_for_database
+ type.serialize(original_value)
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.deserialize(value)
+ end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.cast(value)
+ end
+
+ def came_from_user?
+ !type.value_constructed_by_mass_assignment?(value_before_type_cast)
+ end
+ end
+
+ class WithCastValue < Attribute # :nodoc:
+ def type_cast(value)
+ value
+ end
+
+ def changed_in_place?
+ false
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type.default_value)
+ end
+
+ def type_cast(*)
+ nil
+ end
+
+ def with_type(type)
+ self.class.with_cast_value(name, nil, type)
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ alias_method :with_cast_value, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ UNINITIALIZED_ORIGINAL_VALUE = Object.new
+
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def original_value
+ UNINITIALIZED_ORIGINAL_VALUE
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+
+ def forgetting_assignment
+ dup
+ end
+
+ def with_type(type)
+ self.class.new(name, type)
+ end
+ end
+
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
+ end
+end
diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb
new file mode 100644
index 0000000000..9dc16e882d
--- /dev/null
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class Attribute # :nodoc:
+ class UserProvidedDefault < FromUser # :nodoc:
+ def initialize(name, value, type, database_default)
+ @user_provided_value = value
+ super(name, value, type, database_default)
+ end
+
+ def value_before_type_cast
+ if user_provided_value.is_a?(Proc)
+ @memoized_value_before_type_cast ||= user_provided_value.call
+ else
+ @user_provided_value
+ end
+ end
+
+ def with_type(type)
+ self.class.new(name, user_provided_value, type, original_attribute)
+ end
+
+ def marshal_dump
+ result = [
+ name,
+ value_before_type_cast,
+ type,
+ original_attribute,
+ ]
+ result << value if defined?(@value)
+ result
+ end
+
+ def marshal_load(values)
+ name, user_provided_value, type, original_attribute, value = values
+ @name = name
+ @user_provided_value = user_provided_value
+ @type = type
+ @original_attribute = original_attribute
+ if values.length == 5
+ @value = value
+ end
+ end
+
+ private
+ attr_reader :user_provided_value
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index 62014cd1cd..f0e3458f51 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
module ActiveModel
module AttributeAssignment
@@ -19,34 +21,37 @@ module ActiveModel
# cat = Cat.new
# cat.assign_attributes(name: "Gorby", status: "yawning")
# cat.name # => 'Gorby'
- # cat.status => 'yawning'
+ # cat.status # => 'yawning'
# cat.assign_attributes(status: "sleeping")
# cat.name # => 'Gorby'
- # cat.status => 'sleeping'
+ # cat.status # => 'sleeping'
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
end
- return if new_attributes.nil? || new_attributes.empty?
+ return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
+ alias attributes= assign_attributes
+
private
- def _assign_attributes(attributes)
- attributes.each do |k, v|
- _assign_attribute(k, v)
+ def _assign_attributes(attributes)
+ attributes.each do |k, v|
+ _assign_attribute(k, v)
+ end
end
- end
- def _assign_attribute(k, v)
- if respond_to?("#{k}=")
- public_send("#{k}=", v)
- else
- raise UnknownAttributeError.new(self, k)
+ def _assign_attribute(k, v)
+ setter = :"#{k}="
+ if respond_to?(setter)
+ public_send(setter, v)
+ else
+ raise UnknownAttributeError.new(self, k)
+ end
end
- end
end
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index cc6285f932..d8352343e9 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,5 +1,6 @@
-require 'concurrent/map'
-require 'mutex_m'
+# frozen_string_literal: true
+
+require "concurrent/map"
module ActiveModel
# Raised when an attribute is not defined.
@@ -68,9 +69,8 @@ module ActiveModel
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
included do
- class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false
- self.attribute_aliases = {}
- self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new]
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
end
module ClassMethods
@@ -289,7 +289,7 @@ module ActiveModel
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
- send(generate_method, attr_name)
+ send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s
end
@@ -328,18 +328,15 @@ module ActiveModel
attribute_method_matchers_cache.clear
end
- def generated_attribute_methods #:nodoc:
- @generated_attribute_methods ||= Module.new {
- extend Mutex_m
- }.tap { |mod| include mod }
- end
+ private
+ def generated_attribute_methods
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ end
- protected
- def instance_method_already_implemented?(method_name) #:nodoc:
+ def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
- private
# The methods +method_missing+ and +respond_to?+ of this module are
# invoked often in a typical rails, both of which invoke the method
# +matched_attribute_method+. The latter method iterates through an
@@ -349,11 +346,11 @@ module ActiveModel
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
# this cache).
- def attribute_method_matchers_cache #:nodoc:
+ def attribute_method_matchers_cache
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
- def attribute_method_matchers_matching(method_name) #:nodoc:
+ def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
# will match every time.
@@ -365,16 +362,16 @@ module ActiveModel
# Define a method `name` in `mod` that dispatches to `send`
# 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
+ def define_proxy_call(include_private, mod, name, send, *extra)
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
"define_method(:'#{name}') do |*args|"
end
- extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
- target = if send =~ CALL_COMPILABLE_REGEXP
+ target = if CALL_COMPILABLE_REGEXP.match?(send)
"#{"self." unless include_private}#{send}(#{extra})"
else
"send(:'#{send}', #{extra})"
@@ -393,7 +390,7 @@ module ActiveModel
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
- @prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
@@ -458,12 +455,11 @@ module ActiveModel
end
end
- protected
- def attribute_method?(attr_name) #:nodoc:
+ private
+ def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
- private
# Returns a struct representing the matching attribute method.
# The struct's attributes are prefix, base and suffix.
def matched_attribute_method(method_name)
@@ -474,5 +470,47 @@ module ActiveModel
def missing_attribute(attr_name, stack)
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
+
+ def _read_attribute(attr)
+ __send__(attr)
+ end
+
+ module AttrNames # :nodoc:
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
+
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes in read_attribute.
+ def self.define_attribute_accessor_method(mod, attr_name, writer: false)
+ method_name = "#{attr_name}#{'=' if writer}"
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
+ yield method_name, "'#{attr_name}'.freeze"
+ else
+ safe_name = attr_name.unpack1("h*")
+ const_name = "ATTR_#{safe_name}"
+ const_set(const_name, attr_name) unless const_defined?(const_name)
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
+ attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
+ yield temp_method_name, attr_name_expr
+ mod.send(:alias_method, method_name, temp_method_name)
+ mod.send(:undef_method, temp_method_name)
+ end
+ end
+ end
end
end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..6abf37bd44
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
+module ActiveModel
+ class AttributeMutationTracker # :nodoc:
+ OPTION_NOT_GIVEN = Object.new
+
+ def initialize(attributes)
+ @attributes = attributes
+ @forced_changes = Set.new
+ end
+
+ def changed_attribute_names
+ attr_names.select { |attr_name| changed?(attr_name) }
+ end
+
+ def changed_values
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = attributes[attr_name].original_value
+ end
+ end
+ end
+
+ def changes
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ change = change_to_attribute(attr_name)
+ if change
+ result.merge!(attr_name => change)
+ end
+ end
+ end
+
+ def change_to_attribute(attr_name)
+ attr_name = attr_name.to_s
+ if changed?(attr_name)
+ [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ end
+ end
+
+ def any_changes?
+ attr_names.any? { |attr| changed?(attr) }
+ end
+
+ def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
+ attr_name = attr_name.to_s
+ forced_changes.include?(attr_name) ||
+ attributes[attr_name].changed? &&
+ (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
+ (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name.to_s].changed_in_place?
+ end
+
+ def forget_change(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
+ forced_changes.delete(attr_name)
+ end
+
+ def original_value(attr_name)
+ attributes[attr_name.to_s].original_value
+ end
+
+ def force_change(attr_name)
+ forced_changes << attr_name.to_s
+ end
+
+ private
+ attr_reader :attributes, :forced_changes
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_attribute_names(*)
+ []
+ end
+
+ def changed_values(*)
+ {}
+ end
+
+ def changes(*)
+ {}
+ end
+
+ def change_to_attribute(attr_name)
+ end
+
+ def any_changes?(*)
+ false
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+
+ def original_value(*)
+ end
+
+ def force_change(*)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
new file mode 100644
index 0000000000..a890ee3932
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+require "active_model/attribute_set/builder"
+require "active_model/attribute_set/yaml_encoder"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ delegate :each_value, :fetch, :except, to: :attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def []=(name, value)
+ attributes[name] = value
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def keys
+ attributes.each_key.select { |name| self[name].initialized? }
+ end
+
+ if defined?(JRUBY_VERSION)
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
+ # https://github.com/jruby/jruby/pull/2562
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+ else
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def write_cast_value(name, value)
+ attributes[name] = self[name].with_cast_value(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def deep_dup
+ self.class.allocate.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def accessed
+ attributes.select { |_, attr| attr.has_been_read? }.keys
+ end
+
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
+ def ==(other)
+ attributes == other.attributes
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb
new file mode 100644
index 0000000000..2b1c2206ec
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/builder.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "active_model/attribute"
+
+module ActiveModel
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types, :default_attributes
+
+ def initialize(types, default_attributes = {})
+ @types = types
+ @default_attributes = default_attributes
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
+ AttributeSet.new(attributes)
+ end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
+
+ def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = delegate_hash
+ @default_attributes = default_attributes
+ end
+
+ def key?(key)
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
+ end
+
+ def [](key)
+ delegate_hash[key] || assign_default_value(key)
+ end
+
+ def []=(key, value)
+ if frozen?
+ raise RuntimeError, "Can't modify frozen hash"
+ end
+ delegate_hash[key] = value
+ end
+
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
+ def initialize_dup(_)
+ @delegate_hash = Hash[delegate_hash]
+ super
+ end
+
+ def select
+ keys = types.keys | values.keys | delegate_hash.keys
+ keys.each_with_object({}) do |key, hash|
+ attribute = self[key]
+ if yield(key, attribute)
+ hash[key] = attribute
+ end
+ end
+ end
+
+ def ==(other)
+ if other.is_a?(LazyAttributeHash)
+ materialize == other.materialize
+ else
+ materialize == other
+ end
+ end
+
+ def marshal_dump
+ [@types, @values, @additional_types, @default_attributes, @delegate_hash]
+ end
+
+ def marshal_load(values)
+ if values.is_a?(Hash)
+ empty_hash = {}.freeze
+ initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
+ @materialized = true
+ else
+ initialize(*values)
+ end
+ end
+
+ protected
+ def materialize
+ unless @materialized
+ values.each_key { |key| self[key] }
+ types.each_key { |key| self[key] }
+ unless frozen?
+ @materialized = true
+ end
+ end
+ delegate_hash
+ end
+
+ private
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
+
+ def assign_default_value(name)
+ type = additional_types.fetch(name, types[name])
+ value_present = true
+ value = values.fetch(name) { value_present = false }
+
+ if value_present
+ delegate_hash[name] = Attribute.from_database(name, value, type)
+ elsif types.key?(name)
+ attr = default_attributes[name]
+ if attr
+ delegate_hash[name] = attr.dup
+ else
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
new file mode 100644
index 0000000000..ea1efc160e
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ActiveModel
+ class AttributeSet
+ # Attempts to do more intelligent YAML dumping of an
+ # ActiveModel::AttributeSet to reduce the size of the resulting string
+ class YAMLEncoder # :nodoc:
+ def initialize(default_types)
+ @default_types = default_types
+ end
+
+ def encode(attribute_set, coder)
+ coder["concise_attributes"] = attribute_set.each_value.map do |attr|
+ if attr.type.equal?(default_types[attr.name])
+ attr.with_type(nil)
+ else
+ attr
+ end
+ end
+ end
+
+ def decode(coder)
+ if coder["attributes"]
+ coder["attributes"]
+ else
+ attributes_hash = Hash[coder["concise_attributes"].map do |attr|
+ if attr.type.nil?
+ attr = attr.with_type(default_types[attr.name])
+ end
+ [attr.name, attr]
+ end]
+ AttributeSet.new(attributes_hash)
+ end
+ end
+
+ private
+ attr_reader :default_types
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
new file mode 100644
index 0000000000..c3a446098c
--- /dev/null
+++ b/activemodel/lib/active_model/attributes.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "active_model/attribute_set"
+require "active_model/attribute/user_provided_default"
+
+module ActiveModel
+ module Attributes #:nodoc:
+ extend ActiveSupport::Concern
+ include ActiveModel::AttributeMethods
+
+ included do
+ attribute_method_suffix "="
+ class_attribute :attribute_types, :_default_attributes, instance_accessor: false
+ self.attribute_types = Hash.new(Type.default_value)
+ self._default_attributes = AttributeSet.new({})
+ end
+
+ module ClassMethods
+ def attribute(name, type = Type::Value.new, **options)
+ name = name.to_s
+ if type.is_a?(Symbol)
+ type = ActiveModel::Type.lookup(type, **options.except(:default))
+ end
+ self.attribute_types = attribute_types.merge(name => type)
+ define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
+ define_attribute_method(name)
+ end
+
+ private
+
+ def define_method_attribute=(name)
+ ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
+ generated_attribute_methods, name, writer: true,
+ ) do |temp_method_name, attr_name_expr|
+ generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{temp_method_name}(value)
+ name = #{attr_name_expr}
+ write_attribute(name, value)
+ end
+ RUBY
+ end
+ end
+
+ NO_DEFAULT_PROVIDED = Object.new # :nodoc:
+ private_constant :NO_DEFAULT_PROVIDED
+
+ def define_default_attribute(name, value, type)
+ self._default_attributes = _default_attributes.deep_dup
+ if value == NO_DEFAULT_PROVIDED
+ default_attribute = _default_attributes[name].with_type(type)
+ else
+ default_attribute = Attribute::UserProvidedDefault.new(
+ name,
+ value,
+ type,
+ _default_attributes.fetch(name.to_s) { nil },
+ )
+ end
+ _default_attributes[name] = default_attribute
+ end
+ end
+
+ def initialize(*)
+ @attributes = self.class._default_attributes.deep_dup
+ super
+ end
+
+ def attributes
+ @attributes.to_hash
+ end
+
+ private
+
+ def write_attribute(attr_name, value)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+
+ @attributes.write_from_user(name, value)
+ value
+ end
+
+ def attribute(attr_name)
+ name = if self.class.attribute_alias?(attr_name)
+ self.class.attribute_alias(attr_name).to_s
+ else
+ attr_name.to_s
+ end
+ @attributes.fetch_value(name)
+ end
+
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 0d6a3dc52d..fde3381df2 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
module ActiveModel
# == Active \Model \Callbacks
@@ -56,6 +58,9 @@ module ActiveModel
#
# Would only create the +after_create+ and +before_create+ callback methods in
# your class.
+ #
+ # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
+ #
module Callbacks
def self.extended(base) #:nodoc:
base.class_eval do
@@ -98,12 +103,11 @@ module ActiveModel
# end
# end
#
- # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
- # `!`, `?` or `=`.
+ # NOTE: +method_name+ passed to define_model_callbacks must not end with
+ # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
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]
@@ -122,28 +126,30 @@ module ActiveModel
private
- def _define_before_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("before_#{callback}") do |*args, &block|
- set_callback(:"#{callback}", :before, *args, &block)
+ def _define_before_model_callback(klass, callback)
+ klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ set_callback(:"#{callback}", :before, *args, options, &block)
+ end
end
- end
- def _define_around_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("around_#{callback}") do |*args, &block|
- set_callback(:"#{callback}", :around, *args, &block)
+ def _define_around_model_callback(klass, callback)
+ klass.define_singleton_method("around_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ set_callback(:"#{callback}", :around, *args, options, &block)
+ end
end
- end
- def _define_after_model_callback(klass, callback) #:nodoc:
- klass.define_singleton_method("after_#{callback}") do |*args, &block|
- options = args.extract_options!
- options[:prepend] = true
- conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
- v != false
- }
- options[:if] = Array(options[:if]) << conditional
- set_callback(:"#{callback}", :after, *(args << options), &block)
+ def _define_after_model_callback(klass, callback)
+ klass.define_singleton_method("after_#{callback}") do |*args, **options, &block|
+ options.assert_valid_keys(:if, :unless, :prepend)
+ options[:prepend] = true
+ conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
+ v != false
+ }
+ options[:if] = Array(options[:if]) << conditional
+ set_callback(:"#{callback}", :after, *args, options, &block)
+ end
end
- end
end
end
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 9de6ea65be..82713ddc81 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# == Active \Model \Conversion
#
@@ -40,15 +42,19 @@ 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
# attr_accessor :id
+ #
+ # def initialize(id)
+ # @id = id
+ # end
# end
#
- # person = Person.create(id: 1)
+ # person = Person.new(1)
# person.to_key # => [1]
def to_key
key = respond_to?(:id) && id
@@ -61,15 +67,20 @@ module ActiveModel
# class Person
# include ActiveModel::Conversion
# attr_accessor :id
+ #
+ # def initialize(id)
+ # @id = id
+ # end
+ #
# def persisted?
# true
# end
# end
#
- # person = Person.create(id: 1)
+ # person = Person.new(1)
# person.to_param # => "1"
def to_param
- (persisted? && key = to_key) ? key.join('-') : nil
+ (persisted? && key = to_key) ? key.join("-") : nil
end
# Returns a +string+ identifying the path associated with the object.
@@ -92,7 +103,7 @@ module ActiveModel
@_to_partial_path ||= begin
element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
collection = ActiveSupport::Inflector.tableize(name)
- "#{collection}/#{element}".freeze
+ "#{collection}/#{element}"
end
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 6e2e5afd1b..0d9e761b1e 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,5 +1,8 @@
-require 'active_support/hash_with_indifferent_access'
-require 'active_support/core_ext/object/duplicable'
+# frozen_string_literal: true
+
+require "active_support/hash_with_indifferent_access"
+require "active_support/core_ext/object/duplicable"
+require "active_model/attribute_mutation_tracker"
module ActiveModel
# == Active \Model \Dirty
@@ -26,8 +29,8 @@ module ActiveModel
#
# define_attribute_methods :name
#
- # def initialize(name)
- # @name = name
+ # def initialize
+ # @name = nil
# end
#
# def name
@@ -58,7 +61,7 @@ module ActiveModel
#
# A newly instantiated +Person+ object is unchanged:
#
- # person = Person.new("Uncle Bob")
+ # person = Person.new
# person.changed? # => false
#
# Change the name:
@@ -66,11 +69,11 @@ module ActiveModel
# person.name = 'Bob'
# person.changed? # => true
# person.name_changed? # => true
- # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
- # person.name_was # => "Uncle Bob"
- # person.name_change # => ["Uncle Bob", "Bob"]
+ # person.name_changed?(from: nil, to: "Bob") # => true
+ # person.name_was # => nil
+ # person.name_change # => [nil, "Bob"]
# person.name = 'Bill'
- # person.name_change # => ["Uncle Bob", "Bill"]
+ # person.name_change # => [nil, "Bill"]
#
# Save the changes:
#
@@ -80,9 +83,9 @@ module ActiveModel
#
# Reset the changes:
#
- # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.previous_changes # => {"name" => [nil, "Bill"]}
# person.name_previously_changed? # => true
- # person.name_previous_change # => ["Uncle Bob", "Bill"]
+ # person.name_previous_change # => [nil, "Bill"]
# person.reload!
# person.previous_changes # => {}
#
@@ -119,13 +122,38 @@ 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: '!'
+ attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
+ attribute_method_suffix "_previously_changed?", "_previous_change"
+ attribute_method_affix prefix: "restore_", suffix: "!"
+ end
+
+ def initialize_dup(other) # :nodoc:
+ super
+ if self.class.respond_to?(:_default_attributes)
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ end
+ @mutations_from_database = nil
end
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
+ # Clears dirty data and moves +changes+ to +previously_changed+ and
+ # +mutations_from_database+ to +mutations_before_last_save+ respectively.
+ def changes_applied
+ unless defined?(@attributes)
+ @previously_changed = changes
+ end
+ @mutations_before_last_save = mutations_from_database
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
#
# person.changed? # => false
# person.name = 'bob'
@@ -143,6 +171,60 @@ module ActiveModel
changed_attributes.keys
end
+ # Handles <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
+ !!changes_include?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
+ end
+
+ # Handles <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(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 }
+ end
+
+ # Clears all dirty data: current changes and previous changes.
+ def clear_changes_information
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ forget_attribute_assignments
+ @mutations_from_database = nil
+ end
+
+ def clear_attribute_changes(attr_names)
+ attributes_changed_by_setter.except!(*attr_names)
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
+ end
+
+ # Returns a hash of the attributes with unsaved changes indicating their original
+ # values like <tt>attr => original value</tt>.
+ #
+ # person.name # => "bob"
+ # person.name = 'robert'
+ # person.changed_attributes # => {"name" => "bob"}
+ def changed_attributes
+ # This should only be set by methods which will call changed_attributes
+ # multiple times when it is known that the computed value cannot change.
+ if defined?(@cached_changed_attributes)
+ @cached_changed_attributes
+ else
+ attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
+ end
+ end
+
# Returns a hash of changed attributes indicating their original
# and new values like <tt>attr => [original value, new value]</tt>.
#
@@ -150,7 +232,9 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ cache_changed_attributes do
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ end
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -161,46 +245,51 @@ module ActiveModel
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
+ @previously_changed.merge(mutations_before_last_save.changes)
end
- # Returns a hash of the attributes with unsaved changes indicating their original
- # values like <tt>attr => original value</tt>.
- #
- # person.name # => "bob"
- # person.name = 'robert'
- # person.changed_attributes # => {"name" => "bob"}
- def changed_attributes
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
+ def attribute_changed_in_place?(attr_name) # :nodoc:
+ mutations_from_database.changed_in_place?(attr_name)
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
- end
+ private
+ def clear_attribute_change(attr_name)
+ mutations_from_database.forget_change(attr_name)
+ end
- # Handles <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr) # :nodoc:
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
- end
+ def mutations_from_database
+ unless defined?(@mutations_from_database)
+ @mutations_from_database = nil
+ end
+ @mutations_from_database ||= if defined?(@attributes)
+ ActiveModel::AttributeMutationTracker.new(@attributes)
+ else
+ NullMutationTracker.instance
+ end
+ end
- # Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr, options = {}) #:nodoc:
- previous_changes_include?(attr)
- end
+ def forget_attribute_assignments
+ @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
+ end
- # Restore all previous data of the provided attributes.
- def restore_attributes(attributes = changed)
- attributes.each { |attr| restore_attribute! attr }
- end
+ def mutations_before_last_save
+ @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
+ end
- private
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ clear_changed_attributes_cache
+ end
+
+ def clear_changed_attributes_cache
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
+ end
# Returns +true+ if attr_name is changed, +false+ otherwise.
def changes_include?(attr_name)
- attributes_changed_by_setter.include?(attr_name)
+ attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
end
alias attribute_changed_by_setter? changes_include?
@@ -210,39 +299,28 @@ module ActiveModel
previous_changes.include?(attr_name)
end
- # Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied # :doc:
- @previously_changed = changes
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Clears all dirty data: current changes and previous changes.
- def clear_changes_information # :doc:
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ [changed_attributes[attr], _read_attribute(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)
+ previous_changes[attr]
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
- return if attribute_changed?(attr)
+ unless attribute_changed?(attr)
+ begin
+ value = _read_attribute(attr)
+ value = value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ end
- begin
- value = __send__(attr)
- value = value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
+ set_attribute_was(attr, value)
end
-
- set_attribute_was(attr, value)
+ mutations_from_database.force_change(attr)
end
# Handles <tt>restore_*!</tt> for +method_missing+.
@@ -253,18 +331,13 @@ module ActiveModel
end
end
- # This is necessary because `changed_attributes` might be overridden in
- # other implementations (e.g. in `ActiveRecord`)
- alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
+ def attributes_changed_by_setter
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
# Force an attribute to have a particular "before" value
def set_attribute_was(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
-
- # Remove changes information for the provided attributes.
- def clear_attribute_changes(attributes) # :doc:
- attributes_changed_by_setter.except!(*attributes)
- end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 4726a68f69..9de6b609a3 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,7 +1,9 @@
-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'
+# frozen_string_literal: true
+
+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
@@ -60,6 +62,11 @@ module ActiveModel
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
+ class << self
+ attr_accessor :i18n_full_message # :nodoc:
+ end
+ self.i18n_full_message = false
+
attr_reader :messages, :details
# Pass in the instance of the object that is using the errors object.
@@ -71,8 +78,8 @@ module ActiveModel
# end
def initialize(base)
@base = base
- @messages = Hash.new { |messages, attribute| messages[attribute] = [] }
- @details = Hash.new { |details, attribute| details[attribute] = [] }
+ @messages = apply_default_array({})
+ @details = apply_default_array({})
end
def initialize_dup(other) # :nodoc:
@@ -81,6 +88,30 @@ 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
+
+ # Merges the errors from <tt>other</tt>.
+ #
+ # other - The ActiveModel::Errors instance.
+ #
+ # Examples
+ #
+ # person.errors.merge!(other)
+ def merge!(other)
+ @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
+ @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
@@ -98,49 +129,21 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- messages[attribute].present?
+ attribute = attribute.to_sym
+ messages.key?(attribute) && messages[attribute].present?
end
alias :has_key? :include?
alias :key? :include?
- # Get messages for +key+.
- #
- # person.errors.messages # => {:name=>["cannot be nil"]}
- # person.errors.get(:name) # => ["cannot be nil"]
- # person.errors.get(:age) # => []
- def get(key)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#get is deprecated and will be removed in Rails 5.1.
-
- To achieve the same use model.errors[:#{key}].
- MESSAGE
-
- messages[key]
- end
-
- # Set messages for +key+ to +value+.
- #
- # person.errors[:name] # => ["cannot be nil"]
- # person.errors.set(: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.
-
- Use model.errors.add(:#{key}, #{value.inspect}) instead.
- MESSAGE
-
- messages[key] = value
- end
-
# Delete messages for +key+. Returns the deleted messages.
#
- # person.errors[:name] # => ["cannot be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
# person.errors.delete(:name) # => ["cannot be nil"]
- # person.errors[:name] # => []
+ # person.errors[:name] # => []
def delete(key)
- details.delete(key)
- messages.delete(key)
+ attribute = key.to_sym
+ details.delete(attribute)
+ messages.delete(attribute)
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -152,20 +155,6 @@ module ActiveModel
messages[attribute.to_sym]
end
- # Adds to the supplied attribute the supplied error message.
- #
- # person.errors[:name] = "must be set"
- # person.errors[:name] # => ['must be set']
- def []=(attribute, error)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#[]= is deprecated and will be removed in Rails 5.1.
-
- Use model.errors.add(:#{attribute}, #{error.inspect}) instead.
- MESSAGE
-
- messages[attribute.to_sym] << error
- end
-
# Iterates through each error key, value pair in the error messages hash.
# Yields the attribute and the error for that attribute. If the attribute
# has more than one error message, yields once for each error message.
@@ -202,7 +191,9 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.values # => [["cannot be nil", "must be specified"]]
def values
- messages.values
+ messages.select do |key, value|
+ !value.empty?
+ end.values
end
# Returns all message keys.
@@ -210,7 +201,9 @@ module ActiveModel
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
- messages.keys
+ messages.select do |key, value|
+ !value.empty?
+ end.keys
end
# Returns +true+ if no errors are found, +false+ otherwise.
@@ -234,7 +227,7 @@ module ActiveModel
# # <error>name can't be blank</error>
# # <error>name must be specified</error>
# # </errors>
- def to_xml(options={})
+ def to_xml(options = {})
to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
end
@@ -244,7 +237,7 @@ module ActiveModel
#
# person.errors.as_json # => {:name=>["cannot be nil"]}
# person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
- def as_json(options=nil)
+ def as_json(options = nil)
to_hash(options && options[:full_messages])
end
@@ -255,11 +248,11 @@ module ActiveModel
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
if full_messages
- self.messages.each_with_object({}) do |(attribute, array), messages|
+ messages.each_with_object({}) do |(attribute, array), messages|
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
else
- self.messages.dup
+ without_default_proc(messages)
end
end
@@ -289,9 +282,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 # => {}
#
@@ -306,7 +299,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
@@ -317,58 +310,30 @@ module ActiveModel
messages[attribute.to_sym] << message
end
- # Will add an error message to each of the attributes in +attributes+
- # that is empty.
- #
- # person.errors.add_on_empty(:name)
- # person.errors.messages
- # # => {: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
-
- To achieve the same use:
-
- errors.add(attribute, :empty, options) if value.nil? || value.empty?
- MESSAGE
-
- Array(attributes).each do |attribute|
- value = @base.send(:read_attribute_for_validation, attribute)
- is_empty = value.respond_to?(:empty?) ? value.empty? : false
- add(attribute, :empty, options) if value.nil? || is_empty
- end
- end
-
- # Will add an error message to each of the attributes in +attributes+ that
- # is blank (using Object#blank?).
- #
- # person.errors.add_on_blank(:name)
- # person.errors.messages
- # # => {: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
-
- To achieve the same use:
-
- errors.add(attribute, :empty, options) if value.blank?
- MESSAGE
-
- Array(attributes).each do |attribute|
- value = @base.send(:read_attribute_for_validation, attribute)
- add(attribute, :blank, options) if value.blank?
- end
- end
-
# Returns +true+ if an error on the attribute with the given message is
- # present, +false+ otherwise. +message+ is treated the same as for +add+.
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
#
# person.errors.add :name, :blank
- # person.errors.added? :name, :blank # => true
+ # person.errors.added? :name, :blank # => true
+ # person.errors.added? :name, "can't be blank" # => true
+ #
+ # If the error message requires an option, then it returns +true+ with
+ # the correct option, or +false+ with an incorrect or missing option.
+ #
+ # person.errors.add :name, :too_long, { count: 25 }
+ # person.errors.added? :name, :too_long, count: 25 # => true
+ # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
+ # person.errors.added? :name, :too_long, count: 24 # => false
+ # person.errors.added? :name, :too_long # => false
+ # person.errors.added? :name, "is too long" # => false
def added?(attribute, message = :invalid, options = {})
message = message.call if message.respond_to?(:call)
- message = normalize_message(attribute, message, options)
- self[attribute].include? message
+
+ if message.is_a? Symbol
+ details[attribute.to_sym].include? normalize_detail(message, options)
+ else
+ self[attribute].include? message
+ end
end
# Returns all the full error messages in an array.
@@ -397,21 +362,63 @@ module ActiveModel
# person.errors.full_messages_for(:name)
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
def full_messages_for(attribute)
+ attribute = attribute.to_sym
messages[attribute].map { |message| full_message(attribute, message) }
end
# Returns a full message for a given attribute.
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
+ #
+ # The `"%{attribute} %{message}"` error format can be overridden with either
+ #
+ # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt>
+ # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt>
+ # * <tt>activemodel.errors.models.person.attributes.name.format</tt>
+ # * <tt>activemodel.errors.models.person.format</tt>
+ # * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
- attr_name = attribute.to_s.tr('.', '_').humanize
+ attribute = attribute.to_s
+
+ if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
+ attribute = attribute.remove(/\[\d\]/)
+ parts = attribute.split(".")
+ attribute_name = parts.pop
+ namespace = parts.join("/") unless parts.empty?
+ attributes_scope = "#{@base.class.i18n_scope}.errors.models"
+
+ if namespace
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
+ ]
+ end
+ else
+ defaults = @base.class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
+ ]
+ end
+ end
+
+ defaults.flatten!
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.format"
+ defaults << "%{attribute} %{message}"
+
+ attr_name = attribute.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
- I18n.t(:"errors.format", {
- default: "%{attribute} %{message}",
+
+ I18n.t(defaults.shift,
+ default: defaults,
attribute: attr_name,
- message: message
- })
+ message: message)
end
# Translates an error message in its default scope
@@ -442,21 +449,19 @@ module ActiveModel
type = options.delete(:message) if options[:message].is_a?(Symbol)
if @base.class.respond_to?(:i18n_scope)
- defaults = @base.class.lookup_ancestors.map do |klass|
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
+ i18n_scope = @base.class.i18n_scope.to_s
+ defaults = @base.class.lookup_ancestors.flat_map do |klass|
+ [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
+ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
else
defaults = []
end
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
- defaults.compact!
- defaults.flatten!
-
key = defaults.shift
defaults = options.delete(:message) if options[:message]
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
@@ -465,12 +470,30 @@ module ActiveModel
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)
end
+ def marshal_dump # :nodoc:
+ [@base, without_default_proc(@messages), without_default_proc(@details)]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @base, @messages, @details = array
+ apply_default_array(@messages)
+ apply_default_array(@details)
+ end
+
+ def init_with(coder) # :nodoc:
+ coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
+ @details ||= {}
+ apply_default_array(@messages)
+ apply_default_array(@details)
+ end
+
private
def normalize_message(attribute, message, options)
case message
@@ -481,9 +504,20 @@ 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
+
+ def without_default_proc(hash)
+ hash.dup.tap do |new_h|
+ new_h.default_proc = nil
+ end
+ end
+
+ def apply_default_array(hash)
+ hash.default_proc = proc { |h, key| h[key] = [] }
+ hash
+ end
end
# Raised when a validation cannot be corrected by end users and are considered
@@ -504,7 +538,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 d2c6a89cc2..4b37f80c52 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# Raised when forbidden attributes are used for mass assignment.
#
@@ -15,7 +17,7 @@ module ActiveModel
end
module ForbiddenAttributesProtection # :nodoc:
- protected
+ private
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?)
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 762f4fe939..cef5441e4a 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActiveModel
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 010eaeb170..b7ceabb59a 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Lint
# == Active \Model \Lint \Tests
@@ -20,7 +22,6 @@ module ActiveModel
# to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
# +self+.
module Tests
-
# Passes if the object's model responds to <tt>to_key</tt> and if calling
# this method returns +nil+ when the object is not persisted.
# Fails otherwise.
@@ -28,7 +29,7 @@ module ActiveModel
# <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
# of the model, and is used to a generate unique DOM id for the object.
def test_to_key
- assert model.respond_to?(:to_key), "The model should respond to to_key"
+ assert_respond_to model, :to_key
def model.persisted?() false end
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
end
@@ -43,7 +44,7 @@ module ActiveModel
# tests for this behavior in lint because it doesn't make sense to force
# any of the possible implementation strategies on the implementer.
def test_to_param
- assert model.respond_to?(:to_param), "The model should respond to to_param"
+ assert_respond_to model, :to_param
def model.to_key() [1] end
def model.persisted?() false end
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
@@ -55,7 +56,7 @@ module ActiveModel
# <tt>to_partial_path</tt> is used for looking up partials. For example,
# a BlogPost model might return "blog_posts/blog_post".
def test_to_partial_path
- assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path"
+ assert_respond_to model, :to_partial_path
assert_kind_of String, model.to_partial_path
end
@@ -67,7 +68,7 @@ module ActiveModel
# will route to the create action. If it is persisted, a form for the
# object will route to the update action.
def test_persisted?
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
+ assert_respond_to model, :persisted?
assert_boolean model.persisted?, "persisted?"
end
@@ -78,14 +79,14 @@ module ActiveModel
#
# Check ActiveModel::Naming for more information.
def test_model_naming
- assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
+ assert_respond_to model.class, :model_name
model_name = model.class.model_name
- assert model_name.respond_to?(:to_str)
- assert model_name.human.respond_to?(:to_str)
- assert model_name.singular.respond_to?(:to_str)
- assert model_name.plural.respond_to?(:to_str)
+ assert_respond_to model_name, :to_str
+ assert_respond_to model_name.human, :to_str
+ assert_respond_to model_name.singular, :to_str
+ assert_respond_to model_name.plural, :to_str
- assert model.respond_to?(:model_name), "The model instance should respond to model_name"
+ assert_respond_to model, :model_name
assert_equal model.model_name, model.class.model_name
end
@@ -99,13 +100,13 @@ module ActiveModel
# If localization is used, the strings should be localized for the current
# locale. If no error is present, the method should return an empty array.
def test_errors_aref
- assert model.respond_to?(:errors), "The model should respond to errors"
+ assert_respond_to model, :errors
assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
end
private
def model
- assert @model.respond_to?(:to_model), "The object should respond to to_model"
+ assert_respond_to @model, :to_model
@model.to_model
end
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index dac8d549a7..fc52cd4fdf 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -1,12 +1,13 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
# == Active \Model \Basic \Model
#
# Includes the required interface for an object to interact with
- # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
+ # Action Pack and Action View, using different Active Model modules.
# It includes model name introspections, conversions, translations and
# validations. Besides that, it allows you to initialize the object with a
- # hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
+ # hash of attributes, pretty much like Active Record does.
#
# A minimal implementation could be:
#
@@ -76,7 +77,7 @@ module ActiveModel
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
# person.age # => "18"
- def initialize(attributes={})
+ def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index d86ef6224e..bf23fa3c05 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,7 +1,8 @@
-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'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/introspection"
+require "active_support/core_ext/module/redefine_method"
module ActiveModel
class Name
@@ -48,7 +49,7 @@ module ActiveModel
# :method: <=>
#
# :call-seq:
- # ==(other)
+ # <=>(other)
#
# Equivalent to <tt>String#<=></tt>.
#
@@ -110,6 +111,22 @@ module ActiveModel
# BlogPost.model_name.eql?('Blog Post') # => false
##
+ # :method: match?
+ #
+ # :call-seq:
+ # match?(regexp)
+ #
+ # Equivalent to <tt>String#match?</tt>. Match the class name against the
+ # given regexp. Returns +true+ if there is a match, otherwise +false+.
+ #
+ # class BlogPost
+ # extend ActiveModel::Naming
+ # end
+ #
+ # BlogPost.model_name.match?(/Post/) # => true
+ # BlogPost.model_name.match?(/\d/) # => false
+
+ ##
# :method: to_s
#
# :call-seq:
@@ -130,7 +147,7 @@ module ActiveModel
# to_str()
#
# Equivalent to +to_s+.
- delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
+ delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
:to_str, :as_json, to: :name
# Returns a new ActiveModel::Name instance. By default, the +namespace+
@@ -149,7 +166,7 @@ module ActiveModel
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
- @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace
+ @unnamespaced = @name.sub(/^#{namespace.name}::/, "") if namespace
@klass = klass
@singular = _singularize(@name)
@plural = ActiveSupport::Inflector.pluralize(@singular)
@@ -174,7 +191,7 @@ module ActiveModel
# BlogPost.model_name.human # => "Blog post"
#
# Specify +options+ with additional translating options.
- def human(options={})
+ def human(options = {})
return @human unless @klass.respond_to?(:lookup_ancestors) &&
@klass.respond_to?(:i18n_scope)
@@ -191,9 +208,9 @@ module ActiveModel
private
- def _singularize(string)
- ActiveSupport::Inflector.underscore(string).tr('/'.freeze, '_'.freeze)
- end
+ def _singularize(string)
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
+ end
end
# == Active \Model \Naming
@@ -217,7 +234,7 @@ module ActiveModel
# provided method below, or rolling your own is required.
module Naming
def self.extended(base) #:nodoc:
- base.remove_possible_method :model_name
+ base.silence_redefinition_of_method :model_name
base.delegate :model_name, to: :class
end
@@ -235,7 +252,7 @@ module ActiveModel
# Person.model_name.plural # => "people"
def model_name
@_model_name ||= begin
- namespace = self.parents.detect do |n|
+ namespace = module_parents.detect do |n|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
end
ActiveModel::Name.new(self, namespace)
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 1671eb7bd4..0ed70bd473 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model"
require "rails"
@@ -5,8 +7,14 @@ module ActiveModel
class Railtie < Rails::Railtie # :nodoc:
config.eager_load_namespaces << ActiveModel
+ config.active_model = ActiveSupport::OrderedOptions.new
+
initializer "active_model.secure_password" do
ActiveModel::SecurePassword.min_cost = Rails.env.test?
end
+
+ initializer "active_model.i18n_full_message" do
+ ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false
+ end
end
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 89da74efa8..51d54f34f3 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
- # BCrypt hash function can handle maximum 72 characters, and if we pass
- # password of length more than 72 characters it ignores extra characters.
+ # BCrypt hash function can handle maximum 72 bytes, and if we pass
+ # password of length more than 72 bytes it ignores extra characters.
# Hence need to put a restriction on password length.
MAX_PASSWORD_LENGTH_ALLOWED = 72
@@ -14,15 +16,16 @@ module ActiveModel
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
- # This mechanism requires you to have a +password_digest+ attribute.
+ # This mechanism requires you to have a +XXX_digest+ attribute.
+ # Where +XXX+ is the attribute name of your desired password.
#
# The following validations are added automatically:
# * Password must be present on creation
- # * Password length should be less than or equal to 72 characters
- # * Confirmation of password (using a +password_confirmation+ attribute)
+ # * Password length should be less than or equal to 72 bytes
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
#
- # If password confirmation validation is not needed, simply leave out the
- # value for +password_confirmation+ (i.e. don't provide a form field for
+ # If confirmation validation is not needed, simply leave out the
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
# it). When this attribute has a +nil+ value, the validation will not be
# triggered.
#
@@ -35,9 +38,10 @@ module ActiveModel
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
- # # Schema: User(name:string, password_digest:string)
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
# class User < ActiveRecord::Base
# has_secure_password
+ # has_secure_password :recovery_password, validations: false
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
@@ -46,24 +50,59 @@ module ActiveModel
# user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
# user.save # => true
+ # user.recovery_password = "42password"
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
+ # user.save # => true
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
+ # user.authenticate_recovery_password('42password') # => user
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
- def has_secure_password(options = {})
+ def has_secure_password(attribute = :password, validations: true)
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
- require 'bcrypt'
+ require "bcrypt"
rescue LoadError
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
raise
end
- include InstanceMethodsOnActivation
+ attr_reader attribute
+
+ define_method("#{attribute}=") do |unencrypted_password|
+ if unencrypted_password.nil?
+ self.send("#{attribute}_digest=", nil)
+ elsif !unencrypted_password.empty?
+ instance_variable_set("@#{attribute}", unencrypted_password)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+ end
+ end
+
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
+ end
+
+ # Returns +self+ if the password is correct, otherwise +false+.
+ #
+ # class User < ActiveRecord::Base
+ # has_secure_password validations: false
+ # end
+ #
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
+ # user.save
+ # user.authenticate_password('notright') # => false
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
+ attribute_digest = send("#{attribute}_digest")
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
+ end
+
+ alias_method :authenticate, :authenticate_password if attribute == :password
- if options.fetch(:validations, true)
+ if validations
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
@@ -71,57 +110,13 @@ module ActiveModel
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
- record.errors.add(:password, :blank) unless record.password_digest.present?
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
end
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of :password, allow_blank: true
- end
- end
- end
-
- module InstanceMethodsOnActivation
- # Returns +self+ if the password is correct, otherwise +false+.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
- # user.save
- # user.authenticate('notright') # => false
- # user.authenticate('mUc3m00RsqyRe') # => user
- def authenticate(unencrypted_password)
- BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
- end
-
- attr_reader :password
-
- # Encrypts the password into the +password_digest+ attribute, only if the
- # new password is not empty.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new
- # user.password = nil
- # user.password_digest # => nil
- # user.password = 'mUc3m00RsqyRe'
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
- def password=(unencrypted_password)
- if unencrypted_password.nil?
- self.password_digest = nil
- elsif !unencrypted_password.empty?
- @password = unencrypted_password
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
- self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
end
end
-
- def password_confirmation=(unencrypted_password)
- @password_confirmation = unencrypted_password
- end
end
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 70e10fa06d..c4b7b32291 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -1,5 +1,7 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
module ActiveModel
# == Active \Model \Serialization
@@ -177,7 +179,7 @@ module ActiveModel
return unless includes = options[:include]
unless includes.is_a?(Hash)
- includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
end
includes.each do |association, opts|
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index b66dbf1afe..f77fb98c32 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -1,4 +1,6 @@
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "active_support/json"
module ActiveModel
module Serializers
@@ -10,8 +12,7 @@ module ActiveModel
included do
extend ActiveModel::Naming
- class_attribute :include_root_in_json
- self.include_root_in_json = false
+ class_attribute :include_root_in_json, instance_writer: false, default: false
end
# Returns a hash representing the model. Some configuration can be
@@ -25,13 +26,13 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
#
# ActiveRecord::Base.include_root_in_json = true
#
# user.as_json
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# This behavior can also be achieved by setting the <tt>:root</tt> option
# to +true+ as in:
@@ -39,7 +40,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json(root: true)
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true } }
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
#
# Without any +options+, the returned Hash will include all the model's
# attributes.
@@ -47,7 +48,7 @@ module ActiveModel
# user = User.find(1)
# user.as_json
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true}
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
# the attributes included, and work similar to the +attributes+ method.
@@ -62,14 +63,14 @@ module ActiveModel
#
# user.as_json(methods: :permalink)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "permalink" => "1-konata-izumi" }
#
# To include associations use <tt>:include</tt>:
#
# user.as_json(include: :posts)
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
# # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
#
@@ -80,7 +81,7 @@ module ActiveModel
# only: :body } },
# only: :title } })
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
- # # "created_at" => "2006/08/01", "awesome" => true,
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
# # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
# # "title" => "Welcome to the weblog" },
# # { "comments" => [ { "body" => "Don't think too hard" } ],
@@ -92,11 +93,12 @@ module ActiveModel
include_root_in_json
end
+ hash = serializable_hash(options).as_json
if root
root = model_name.element if root == true
- { root => serializable_hash(options) }
+ { root => hash }
else
- serializable_hash(options)
+ hash
end
end
@@ -134,7 +136,7 @@ module ActiveModel
# person.name # => "bob"
# person.age # => 22
# person.awesome # => true
- def from_json(json, include_root=include_root_in_json)
+ def from_json(json, include_root = include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
self.attributes = hash
diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb
deleted file mode 100644
index 5004855d56..0000000000
--- a/activemodel/lib/active_model/test_case.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-module ActiveModel #:nodoc:
- class TestCase < ActiveSupport::TestCase #:nodoc:
- end
-end
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index 8470915abb..f3d0d3dc27 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
# == Active \Model \Translation
#
# Provides integration between your object and the Rails internationalization
@@ -31,7 +32,7 @@ module ActiveModel
# ActiveModel::Errors#full_messages and
# ActiveModel::Translation#human_attribute_name.
def lookup_ancestors
- self.ancestors.select { |x| x.respond_to?(:model_name) }
+ ancestors.select { |x| x.respond_to?(:model_name) }
end
# Transforms attribute names into a more human format, such as "First name"
@@ -45,7 +46,7 @@ module ActiveModel
parts = attribute.to_s.split(".")
attribute = parts.pop
namespace = parts.join("/") unless parts.empty?
- attributes_scope = "#{self.i18n_scope}.attributes"
+ attributes_scope = "#{i18n_scope}.attributes"
if namespace
defaults = lookup_ancestors.map do |klass|
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index bec851594f..1d7a26fff5 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -1,22 +1,21 @@
-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'
+# frozen_string_literal: true
+
+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/float"
+require "active_model/type/immutable_string"
+require "active_model/type/integer"
+require "active_model/type/string"
+require "active_model/type/time"
+
+require "active_model/type/registry"
module ActiveModel
module Type
@@ -24,16 +23,8 @@ module ActiveModel
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.
+
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -41,19 +32,22 @@ module ActiveModel
def lookup(*args, **kwargs) # :nodoc:
registry.lookup(*args, **kwargs)
end
+
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ 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(: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
index 4168cbfce7..89e43bcc5f 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,13 +1,15 @@
-require 'active_model/type/integer'
+# frozen_string_literal: true
+
+require "active_model/type/integer"
module ActiveModel
module Type
class BigInteger < Integer # :nodoc:
private
- def max_value
- ::Float::INFINITY
- end
+ 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
index a0cc45b4c3..76203c5a88 100644
--- a/activemodel/lib/active_model/type/binary.rb
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Binary < Value # :nodoc:
@@ -38,7 +40,7 @@ module ActiveModel
alias_method :to_str, :to_s
def hex
- @value.unpack('H*')[0]
+ @value.unpack1("H*")
end
def ==(other)
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index c1bce98c87..f6c6efbc87 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -1,21 +1,38 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- class Boolean < Value # :nodoc:
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
+ # == Active \Model \Type \Boolean
+ #
+ # A class that behaves like a boolean type, including rules for coercion of user input.
+ #
+ # === Coercion
+ # Values set from user input will first be coerced into the appropriate ruby type.
+ # Coercion behavior is roughly mapped to Ruby's boolean semantics.
+ #
+ # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
+ # - Empty strings are coerced to +nil+
+ # - All other values will be coerced to +true+
+ class Boolean < Value
+ FALSE_VALUES = [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"].to_set
- def type
+ def type # :nodoc:
:boolean
end
+ def serialize(value) # :nodoc:
+ cast(value)
+ end
+
private
- def cast_value(value)
- if value == ''
- nil
- else
- !FALSE_VALUES.include?(value)
+ def cast_value(value)
+ if value == ""
+ nil
+ else
+ !FALSE_VALUES.include?(value)
+ end
end
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index f74243a22c..8ec5deedc4 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Date < Value # :nodoc:
@@ -7,44 +9,48 @@ module ActiveModel
:date
end
+ def serialize(value)
+ cast(value)
+ end
+
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
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
+ 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
- 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
+ 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
- end
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- 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
+ def new_date(year, mon, mday)
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
+ ::Date.new(year, mon, mday) rescue nil
+ end
end
- end
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- 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
index 2f2df4320f..d48598376e 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class DateTime < Value # :nodoc:
@@ -10,35 +12,39 @@ module ActiveModel
:datetime
end
+ def serialize(value)
+ super(cast(value))
+ end
+
private
- def cast_value(value)
- return apply_seconds_precision(value) unless value.is_a?(::String)
- return if value.empty?
+ 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
+ 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
+ # '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)
+ 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
+ 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
+ def value_from_multiparameter_assignment(values_hash)
+ missing_parameters = (1..3).select { |key| !values_hash.key?(key) }
+ if missing_parameters.any?
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
+ end
+ super
end
- super
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index d19d8baada..b37dad1c41 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -1,52 +1,74 @@
+# frozen_string_literal: true
+
require "bigdecimal/util"
module ActiveModel
module Type
class Decimal < Value # :nodoc:
include Helpers::Numeric
+ BIGDECIMAL_PRECISION = 18
def type
:decimal
end
+ def serialize(value)
+ cast(value)
+ 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
+ def cast_value(value)
+ casted_value = \
+ case value
+ when ::Float
+ convert_float_to_big_decimal(value)
+ when ::Numeric
+ BigDecimal(value, precision || BIGDECIMAL_PRECISION)
+ when ::String
+ begin
+ value.to_d
+ rescue ArgumentError
+ BigDecimal(0)
+ end
+ 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
- cast_value(value.to_s)
+ value.to_d
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
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
end
- end
- def float_precision
- if precision.to_i > ::Float::DIG + 1
- ::Float::DIG + 1
- else
- precision.to_i
+ def apply_scale(value)
+ if scale
+ value.round(scale)
+ else
+ value
+ end
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
deleted file mode 100644
index 129baa0c10..0000000000
--- a/activemodel/lib/active_model/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-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
index 0f925bc7e1..9dbe32e5a6 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Float < Value # :nodoc:
@@ -7,19 +9,28 @@ module ActiveModel
:float
end
+ def type_cast_for_schema(value)
+ return "::Float::NAN" if value.try(:nan?)
+ case value
+ when ::Float::INFINITY then "::Float::INFINITY"
+ when -::Float::INFINITY then "-::Float::INFINITY"
+ else super
+ end
+ 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
+ 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
end
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index a805a359ab..403f0a9e6b 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -1,4 +1,6 @@
-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'
+# frozen_string_literal: true
+
+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
index facea12704..ad891f841e 100644
--- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
+ module Helpers # :nodoc: all
+ class AcceptsMultiparameterTime < Module
def initialize(defaults: {})
define_method(:cast) do |value|
if value.is_a?(Hash)
@@ -19,6 +21,10 @@ module ActiveModel
end
end
+ define_method(:value_constructed_by_mass_assignment?) do |value|
+ value.is_a?(Hash)
+ end
+
define_method(:value_from_multiparameter_assignment) do |values_hash|
defaults.each do |k, v|
values_hash[k] ||= v
diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
index 4dddbe4e5e..1cbea644c4 100644
--- a/activemodel/lib/active_model/type/helpers/mutable.rb
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- module Mutable # :nodoc:
+ module Helpers # :nodoc: all
+ module Mutable
def cast(value)
deserialize(serialize(value))
end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index c883010506..473cdb0c67 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -1,14 +1,17 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- module Numeric # :nodoc:
+ module Helpers # :nodoc: all
+ module Numeric
def cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
+ value = \
+ case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
super(value)
end
@@ -18,16 +21,16 @@ module ActiveModel
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 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
+ 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.
+ !/\A[-+]?\d+/.match?(value.to_s)
+ 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
index 63993c0d93..da56073436 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/zones"
require "active_support/core_ext/time/zones"
module ActiveModel
module Type
- module Helpers
- module TimeValue # :nodoc:
+ module Helpers # :nodoc: all
+ module TimeValue
def serialize(value)
value = apply_seconds_precision(value)
@@ -19,7 +22,7 @@ module ActiveModel
end
def is_utc?
- ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC'
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
end
def default_timezone
@@ -33,12 +36,12 @@ module ActiveModel
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)
+ round_power = 10**number_of_insignificant_digits
+ value.change(usec: value.usec - value.usec % round_power)
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
end
def user_input_in_time_zone(value)
@@ -47,30 +50,36 @@ module ActiveModel
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)
+ 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
+ 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
+ time -= offset
+ is_utc? ? time : time.getlocal
+ else
+ ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
end
- end
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+ 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
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ISO_DATETIME
+ microsec_part = $7
+ if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7
+ microsec_part[0] = ""
+ microsec = microsec_part.to_i
+ else
+ microsec = (microsec_part.to_r * 1_000_000).to_i
+ end
+ 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
index 20b8ca0cc4..826bd7038f 100644
--- a/activemodel/lib/active_model/type/immutable_string.rb
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class ImmutableString < Value # :nodoc:
@@ -16,14 +18,15 @@ module ActiveModel
private
- def cast_value(value)
- result = case value
- when true then "t"
- when false then "f"
- else value.to_s
- end
- result.freeze
- end
+ 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
index 2f73ede009..da74aaa3c5 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
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.
+ # 4 bytes means an integer as opposed to smallint etc.
DEFAULT_LIMIT = 4
def initialize(*)
@@ -29,38 +31,35 @@ module ActiveModel
result
end
- protected
-
- attr_reader :range
-
private
+ attr_reader :range
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else
+ value.to_i rescue nil
+ end
end
- end
- def ensure_in_range(value)
- unless range.cover?(value)
- raise RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
+ end
end
- end
- def max_value
- 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
- 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 min_value
+ -max_value
+ end
- def _limit
- self.limit || DEFAULT_LIMIT
- end
+ def _limit
+ 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
index adc88eb624..a19dc0f011 100644
--- a/activemodel/lib/active_model/type/registry.rb
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# :stopdoc:
module Type
@@ -21,19 +23,16 @@ module ActiveModel
end
end
- protected
-
- attr_reader :registrations
-
private
+ attr_reader :registrations
- def registration_klass
- Registration
- end
+ def registration_klass
+ Registration
+ end
- def find_registration(symbol, *args)
- registrations.find { |r| r.matches?(symbol, *args) }
- end
+ def find_registration(symbol, *args)
+ registrations.find { |r| r.matches?(symbol, *args) }
+ end
end
class Registration
@@ -55,9 +54,8 @@ module ActiveModel
type_name == name
end
- protected
-
- attr_reader :name, :block
+ private
+ attr_reader :name, :block
end
end
# :startdoc:
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 8a91410998..a9c9bfadb6 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/type/immutable_string"
module ActiveModel
@@ -11,9 +13,14 @@ module ActiveModel
private
- def cast_value(value)
- ::String.new(super)
- end
+ def cast_value(value)
+ case value
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else 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
deleted file mode 100644
index 1ad04daba4..0000000000
--- a/activemodel/lib/active_model/type/text.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-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
index 7101bad566..b3056b1333 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Time < Value # :nodoc:
@@ -16,6 +18,8 @@ module ActiveModel
case value
when ::String
value = "2000-01-01 #{value}"
+ time_hash = ::Date._parse(value)
+ return if time_hash[:hour].nil?
when ::Time
value = value.change(year: 2000, day: 1, month: 1)
end
@@ -25,18 +29,18 @@ module ActiveModel
private
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
- dummy_time_value = "2000-01-01 #{value}"
+ dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
- 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))
+ 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
end
diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb
deleted file mode 100644
index 3f49f9f5f7..0000000000
--- a/activemodel/lib/active_model/type/unsigned_integer.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-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
index 5fea0561a6..b6914dd63c 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Value
@@ -84,24 +86,41 @@ module ActiveModel
false
end
+ def value_constructed_by_mass_assignment?(_value) # :nodoc:
+ false
+ end
+
+ def force_equality?(_value) # :nodoc:
+ 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
+ # 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 f23c920d87..7f14d102dd 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,9 +1,10 @@
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/except'
+# frozen_string_literal: true
-module ActiveModel
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/hash/except"
+module ActiveModel
# == Active \Model \Validations
#
# Provides a full validation framework to your objects.
@@ -47,10 +48,10 @@ module ActiveModel
include HelperMethods
attr_accessor :validation_context
+ private :validation_context=
define_callbacks :validate, scope: :name
- class_attribute :_validators
- self._validators = Hash.new { |h,k| h[k] = [] }
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
end
module ClassMethods
@@ -68,7 +69,7 @@ module ActiveModel
#
# 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
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -134,7 +135,7 @@ module ActiveModel
#
# 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
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -147,6 +148,9 @@ module ActiveModel
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or +false+
# value.
+ #
+ # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
+ #
def validate(*args, &block)
options = args.extract_options!
@@ -160,14 +164,14 @@ module ActiveModel
if options.key?(:on)
options = options.dup
+ options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- !(Array(options[:on]) & Array(o.validation_context)).empty?
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- args << options
- set_callback(:validate, *args, &block)
+ set_callback(:validate, *args, options, &block)
end
# List all validators that are being used to validate the model using
@@ -303,8 +307,6 @@ module ActiveModel
# Runs all the specified validations and returns +true+ if no errors were
# added otherwise +false+.
#
- # Aliased as validate.
- #
# class Person
# include ActiveModel::Validations
#
@@ -401,14 +403,14 @@ module ActiveModel
# end
alias :read_attribute_for_validation :send
- protected
+ private
- def run_validations! #:nodoc:
+ def run_validations!
_run_validate_callbacks
errors.empty?
end
- def raise_validation_error
+ def raise_validation_error # :doc:
raise(ValidationError.new(self))
end
end
@@ -434,4 +436,4 @@ module ActiveModel
end
end
-Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
+Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
index 75bf655578..385d9f27e0 100644
--- a/activemodel/lib/active_model/validations/absence.rb
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
# == \Active \Model Absence Validator
@@ -22,7 +24,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_absence_of(*attr_names)
validates_with AbsenceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index c5c0cd4636..ea3a6b52ab 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class AcceptanceValidator < EachValidator # :nodoc:
def initialize(options)
@@ -15,62 +16,55 @@ module ActiveModel
private
- def setup!(klass)
- klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
- end
+ def setup!(klass)
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
+ end
- def acceptable_option?(value)
- Array(options[:accept]).include?(value)
- 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
+ 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)
+ 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
- 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
+ class AttributeDefinition
+ def initialize(attributes)
+ @attributes = attributes.map(&:to_s)
+ end
- protected
+ def matches?(method_name)
+ attr_name = convert_to_reader_name(method_name)
+ attributes.include?(attr_name)
+ end
- attr_reader :attributes
+ 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
- private
+ private
+ attr_reader :attributes
- 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
+ def convert_to_reader_name(method_name)
+ method_name.to_s.chomp("=")
+ end
end
- end
end
module HelperMethods
@@ -98,7 +92,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information.
+ # See <tt>ActiveModel::Validations#validates</tt> for more information.
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 52111e5442..887d31ae2a 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
# == Active \Model \Validation \Callbacks
@@ -23,14 +25,12 @@ 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
@@ -54,19 +54,21 @@ module ActiveModel
# person.valid? # => true
# person.name # => "bob"
def before_validation(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
+ options = args.extract_options!
+
+ if options.key?(:on)
+ options = options.dup
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :before, *args, &block)
+
+ set_callback(:validation, :before, *args, options, &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
@@ -94,22 +96,25 @@ module ActiveModel
# person.status # => true
def after_validation(*args, &block)
options = args.extract_options!
+ options = options.dup
options[:prepend] = true
- options[:if] = Array(options[:if])
- if options[:on]
+
+ if options.key?(:on)
options[:on] = Array(options[:on])
+ options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- options[:on].include? o.validation_context
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- set_callback(:validation, :after, *(args << options), &block)
+
+ set_callback(:validation, :after, *args, options, &block)
end
end
- protected
+ private
# Overwrite run validations to include callbacks.
- def run_validations! #:nodoc:
+ def run_validations!
_run_validation_callbacks { super }
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index bad9e4f9a9..bafb8e2106 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/range'
+# frozen_string_literal: true
+
+require "active_support/core_ext/range"
module ActiveModel
module Validations
@@ -16,12 +18,12 @@ module ActiveModel
def include?(record, value)
members = if delimiter.respond_to?(:call)
- delimiter.call(record)
- elsif delimiter.respond_to?(:to_sym)
- record.send(delimiter)
- else
- delimiter
- end
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
members.send(inclusion_method(members), value)
end
@@ -30,14 +32,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
+ # After 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 8f8ade90bb..1b5d5b09ab 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class ConfirmationValidator < EachValidator # :nodoc:
def initialize(options)
@@ -8,7 +9,7 @@ module ActiveModel
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation"))
+ unless (confirmed = record.send("#{attribute}_confirmation")).nil?
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))
@@ -17,23 +18,23 @@ module ActiveModel
end
private
- def setup!(klass)
- klass.send(:attr_reader, *attributes.map do |attribute|
- :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
- end.compact)
+ def setup!(klass)
+ klass.send(:attr_reader, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
+ end.compact)
- klass.send(:attr_writer, *attributes.map do |attribute|
- :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
- end.compact)
- end
+ klass.send(:attr_writer, *attributes.map do |attribute|
+ :"#{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
+ 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
end
module HelperMethods
@@ -70,7 +71,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index 6f4276cc2a..3be7ab6ba8 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
-
module Validations
class ExclusionValidator < EachValidator # :nodoc:
include Clusivity
@@ -39,7 +40,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_exclusion_of(*attr_names)
validates_with ExclusionValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 46a2e54fba..7c3f091473 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class FormatValidator < EachValidator # :nodoc:
def validate_each(record, attribute, value)
@@ -8,7 +9,7 @@ module ActiveModel
record_error(record, attribute, :with, value) if value.to_s !~ regexp
elsif options[:without]
regexp = option_call(record, :without)
- record_error(record, attribute, :without, value) if value.to_s =~ regexp
+ record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
end
end
@@ -23,33 +24,33 @@ module ActiveModel
private
- def option_call(record, name)
- option = options[name]
- option.respond_to?(:call) ? option.call(record) : option
- end
+ def option_call(record, name)
+ option = options[name]
+ option.respond_to?(:call) ? option.call(record) : option
+ end
- def record_error(record, attribute, name, value)
- record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
- end
+ def record_error(record, attribute, name, value)
+ record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
+ end
- def check_options_validity(name)
- if option = options[name]
- if option.is_a?(Regexp)
- if options[:multiline] != true && regexp_using_multiline_anchors?(option)
- raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
- "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
- ":multiline => true option?"
+ def check_options_validity(name)
+ if option = options[name]
+ if option.is_a?(Regexp)
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
+ raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
+ "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
+ ":multiline => true option?"
+ end
+ elsif !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
- elsif !option.respond_to?(:call)
- raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
end
- end
- def regexp_using_multiline_anchors?(regexp)
- source = regexp.source
- source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
- end
+ def regexp_using_multiline_anchors?(regexp)
+ source = regexp.source
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
+ end
end
module HelperMethods
@@ -104,7 +105,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_format_of(*attr_names)
validates_with FormatValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb
index 2176115334..730173f2f9 100644
--- a/activemodel/lib/active_model/validations/helper_methods.rb
+++ b/activemodel/lib/active_model/validations/helper_methods.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
module HelperMethods # :nodoc:
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 03e0ef56d8..9c12dc14c5 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
-
module Validations
class InclusionValidator < EachValidator # :nodoc:
include Clusivity
@@ -18,7 +19,7 @@ module ActiveModel
# particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, in: %w( m f )
+ # validates_inclusion_of :role, in: %w( admin contributor )
# validates_inclusion_of :age, in: 0..99
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
@@ -37,7 +38,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_inclusion_of(*attr_names)
validates_with InclusionValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index 910cca2f49..d6c80b2c5d 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,4 +1,4 @@
-require "active_support/core_ext/string/strip"
+# frozen_string_literal: true
module ActiveModel
module Validations
@@ -6,7 +6,7 @@ module ActiveModel
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
- RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
@@ -18,27 +18,6 @@ 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
@@ -46,20 +25,19 @@ module ActiveModel
keys = CHECKS.keys & options.keys
if keys.empty?
- raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
+ raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
end
keys.each do |key|
value = options[key]
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
- raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
+ raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
end
end
end
def validate_each(record, attribute, value)
- value = tokenize(record, value)
value_length = value.respond_to?(:length) ? value.length : value.to_s.length
errors_options = options.except(*RESERVED_OPTIONS)
@@ -67,6 +45,12 @@ module ActiveModel
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
+ case check_value
+ when Proc
+ check_value = check_value.call(record)
+ when Symbol
+ check_value = record.send(check_value)
+ end
next if value_length.send(validity_check, check_value)
end
@@ -80,24 +64,12 @@ module ActiveModel
end
private
- def tokenize(record, value)
- tokenizer = options[:tokenizer]
- if tokenizer && value.kind_of?(String)
- if tokenizer.kind_of?(Proc)
- tokenizer.call(value)
- elsif record.respond_to?(tokenizer)
- record.send(tokenizer, value)
- end
- end || value
- end
-
- def skip_nil_check?(key)
- key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
- end
+ def skip_nil_check?(key)
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
+ end
end
module HelperMethods
-
# 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:
@@ -136,7 +108,7 @@ module ActiveModel
# * <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)").
@@ -146,7 +118,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+ and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_length_of(*attr_names)
validates_with LengthValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 9c1e8b4ba7..126a87ac6e 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
@@ -18,17 +19,26 @@ module ActiveModel
end
def validate_each(record, attr_name, value)
- before_type_cast = :"#{attr_name}_before_type_cast"
+ came_from_user = :"#{attr_name}_came_from_user?"
- raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value
+ if record.respond_to?(came_from_user)
+ if record.public_send(came_from_user)
+ raw_value = record.read_attribute_before_type_cast(attr_name)
+ elsif record.respond_to?(:read_attribute)
+ raw_value = record.read_attribute(attr_name)
+ end
+ else
+ before_type_cast = :"#{attr_name}_before_type_cast"
+ if record.respond_to?(before_type_cast)
+ raw_value = record.public_send(before_type_cast)
+ end
+ end
raw_value ||= value
if record_attribute_changed_in_place?(record, attr_name)
raw_value = value
end
- return if options[:allow_nil] && raw_value.nil?
-
unless is_number?(raw_value)
record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
return
@@ -39,6 +49,12 @@ module ActiveModel
return
end
+ if raw_value.is_a?(Numeric)
+ value = raw_value
+ else
+ value = parse_raw_value_as_a_number(raw_value)
+ end
+
options.slice(*CHECKS.keys).each do |option, option_value|
case option
when :odd, :even
@@ -60,15 +76,19 @@ module ActiveModel
end
end
- protected
+ private
def is_number?(raw_value)
- parsed_value = Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
- !parsed_value.nil?
+ !parse_raw_value_as_a_number(raw_value).nil?
rescue ArgumentError, TypeError
false
end
+ def parse_raw_value_as_a_number(raw_value)
+ return raw_value.to_i if is_integer?(raw_value)
+ Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ end
+
def is_integer?(raw_value)
/\A[+-]?\d+\z/ === raw_value.to_s
end
@@ -90,8 +110,6 @@ module ActiveModel
end
end
- private
-
def record_attribute_changed_in_place?(record, attr_name)
record.respond_to?(:attribute_changed_in_place?) &&
record.attribute_changed_in_place?(attr_name.to_s)
@@ -101,7 +119,7 @@ module ActiveModel
module HelperMethods
# Validates whether the value of the specified attribute is numeric by
# trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
- # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt>
+ # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
# (if <tt>only_integer</tt> is set to +true+).
#
# class Person < ActiveRecord::Base
@@ -113,7 +131,7 @@ module ActiveModel
# * <tt>:only_integer</tt> - Specifies whether the value has to be an
# integer, e.g. an integral value (default is +false+).
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
- # +false+). Notice that for fixnum and float columns empty strings are
+ # +false+). Notice that for Integer and Float columns empty strings are
# converted to +nil+.
# * <tt>:greater_than</tt> - Specifies the value must be greater than the
# supplied value.
@@ -132,7 +150,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
#
# The following checks can also be supplied with a proc or a symbol which
# corresponds to a method:
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
index 5d593274eb..8787a75afa 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -1,6 +1,6 @@
+# frozen_string_literal: true
module ActiveModel
-
module Validations
class PresenceValidator < EachValidator # :nodoc:
def validate_each(record, attr_name, value)
@@ -30,7 +30,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 1da4df21e7..21c4ce0dfe 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
module ActiveModel
module Validations
@@ -18,7 +20,6 @@ module ActiveModel
# validates :first_name, length: { maximum: 30 }
# validates :age, numericality: true
# validates :username, presence: true
- # validates :username, uniqueness: true
#
# The power of the +validates+ method comes when using custom validators
# and default validators in one call for a given attribute.
@@ -34,7 +35,7 @@ module ActiveModel
# include ActiveModel::Validations
# attr_accessor :name, :email
#
- # validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
+ # validates :name, presence: true, length: { maximum: 100 }
# validates :email, presence: true, email: true
# end
#
@@ -62,7 +63,7 @@ module ActiveModel
# and strings in shortcut form.
#
# validates :email, format: /@/
- # validates :gender, inclusion: %w(male female)
+ # validates :role, inclusion: %(admin contributor)
# validates :password, length: 6..20
#
# When using shortcut form, ranges and arrays are passed to your
@@ -72,7 +73,7 @@ module ActiveModel
# There is also a list of options that could be used along with validators:
#
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
- # Runs in all validation contexts by default (nil). You can pass a symbol
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -94,7 +95,7 @@ module ActiveModel
# Example:
#
# validates :password, presence: true, confirmation: true, if: :password_required?
- # validates :token, uniqueness: true, strict: TokenGenerationException
+ # validates :token, length: 24, strict: TokenLengthException
#
#
# Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
@@ -115,7 +116,7 @@ module ActiveModel
key = "#{key.to_s.camelize}Validator"
begin
- validator = key.include?('::'.freeze) ? key.constantize : const_get(key)
+ validator = key.include?("::") ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
@@ -148,15 +149,15 @@ module ActiveModel
validates(*(attributes << options))
end
- protected
+ private
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
- def _validates_default_keys # :nodoc:
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ def _validates_default_keys
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
end
- def _parse_validates_options(options) # :nodoc:
+ def _parse_validates_options(options)
case options
when TrueClass
{}
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index 6de01b3392..d777ac836e 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+
module ActiveModel
module Validations
class WithValidator < EachValidator # :nodoc:
@@ -43,7 +47,7 @@ module ActiveModel
#
# Configuration 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
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
# or an array of symbols. (e.g. <tt>on: :create</tt> or
# <tt>on: :custom_validation_context</tt> or
# <tt>on: [:create, :custom_validation_context]</tt>)
@@ -59,7 +63,7 @@ module ActiveModel
# The method, proc or string should return or evaluate to a +true+ or
# +false+ value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
- # See <tt>ActiveModel::Validation#validates!</tt> for more information.
+ # See <tt>ActiveModel::Validations#validates!</tt> for more information.
#
# If you pass any additional configuration options, they will be passed
# to the class and available as +options+:
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 1d2888a818..e17c3ca7b3 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -1,7 +1,8 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/module/anonymous"
module ActiveModel
-
# == Active \Model \Validator
#
# A simple base class that can be used along with
@@ -83,7 +84,7 @@ module ActiveModel
# end
#
# It can be useful to access the class that is using that validator when there are prerequisites such
- # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
+ # as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor.
# To setup your validator override the constructor.
#
# class MyValidator < ActiveModel::Validator
@@ -98,20 +99,20 @@ module ActiveModel
# Returns the kind of the validator.
#
# PresenceValidator.kind # => :presence
- # UniquenessValidator.kind # => :uniqueness
+ # AcceptanceValidator.kind # => :acceptance
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.
def initialize(options = {})
- @options = options.except(:class).freeze
+ @options = options.except(:class).freeze
end
# Returns the kind for this validator.
#
- # PresenceValidator.new.kind # => :presence
- # UniquenessValidator.new.kind # => :uniqueness
+ # PresenceValidator.new(attributes: [:username]).kind # => :presence
+ # AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance
def kind
self.class.kind
end
@@ -142,8 +143,8 @@ module ActiveModel
end
# Performs validation on the supplied record. By default this will call
- # +validates_each+ to determine validity therefore subclasses should
- # override +validates_each+ with validation logic.
+ # +validate_each+ to determine validity therefore subclasses should
+ # override +validate_each+ with validation logic.
def validate(record)
attributes.each do |attribute|
value = record.read_attribute_for_validation(attribute)
@@ -163,10 +164,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
@@ -179,8 +176,8 @@ module ActiveModel
private
- def validate_each(record, attribute, value)
- @block.call(record, attribute, value)
- end
+ def validate_each(record, attribute, value)
+ @block.call(record, attribute, value)
+ end
end
end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 6da3b4117b..dd817f5639 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActiveModel
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>