aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md128
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc4
-rw-r--r--activemodel/activemodel.gemspec7
-rw-r--r--activemodel/lib/active_model.rb2
-rw-r--r--activemodel/lib/active_model/attribute.rb1
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb2
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb40
-rw-r--r--activemodel/lib/active_model/attribute_set.rb12
-rw-r--r--activemodel/lib/active_model/attributes.rb32
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/conversion.rb2
-rw-r--r--activemodel/lib/active_model/dirty.rb6
-rw-r--r--activemodel/lib/active_model/errors.rb90
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb22
-rw-r--r--activemodel/lib/active_model/railtie.rb4
-rw-r--r--activemodel/lib/active_model/secure_password.rb36
-rw-r--r--activemodel/lib/active_model/serializers/json.rb19
-rw-r--r--activemodel/lib/active_model/type/boolean.rb11
-rw-r--r--activemodel/lib/active_model/type/date.rb7
-rw-r--r--activemodel/lib/active_model/type/date_time.rb11
-rw-r--r--activemodel/lib/active_model/type/float.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers.rb1
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb11
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb20
-rw-r--r--activemodel/lib/active_model/type/helpers/timezone.rb19
-rw-r--r--activemodel/lib/active_model/type/integer.rb22
-rw-r--r--activemodel/lib/active_model/type/string.rb4
-rw-r--r--activemodel/lib/active_model/type/time.rb3
-rw-r--r--activemodel/lib/active_model/validations.rb2
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb5
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb4
-rw-r--r--activemodel/lib/active_model/validations/length.rb2
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb46
-rw-r--r--activemodel/lib/active_model/validations/validates.rb2
-rw-r--r--activemodel/lib/active_model/validator.rb2
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb4
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb90
-rw-r--r--activemodel/test/cases/attribute_set_test.rb2
-rw-r--r--activemodel/test/cases/attribute_test.rb6
-rw-r--r--activemodel/test/cases/callbacks_test.rb2
-rw-r--r--activemodel/test/cases/dirty_test.rb18
-rw-r--r--activemodel/test/cases/errors_test.rb114
-rw-r--r--activemodel/test/cases/helper.rb20
-rw-r--r--activemodel/test/cases/railtie_test.rb16
-rw-r--r--activemodel/test/cases/secure_password_test.rb32
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb52
-rw-r--r--activemodel/test/cases/type/boolean_test.rb14
-rw-r--r--activemodel/test/cases/type/date_test.rb16
-rw-r--r--activemodel/test/cases/type/date_time_test.rb11
-rw-r--r--activemodel/test/cases/type/decimal_test.rb5
-rw-r--r--activemodel/test/cases/type/float_test.rb5
-rw-r--r--activemodel/test/cases/type/integer_test.rb21
-rw-r--r--activemodel/test/cases/type/string_test.rb12
-rw-r--r--activemodel/test/cases/type/time_test.rb1
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb34
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb75
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb40
-rw-r--r--activemodel/test/models/topic.rb14
-rw-r--r--activemodel/test/models/user.rb7
64 files changed, 868 insertions, 341 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 8f80838a33..ad87abfa3a 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,8 +1,128 @@
+* Type cast falsy boolean symbols on boolean attribute as false.
+
+ Fixes #35676.
+
+ *Ryuta Kamizono*
+
+* Change how validation error translation strings are fetched: The new behavior
+ will first try the more specific keys, including doing locale fallback, then try
+ the less specific ones.
+
+ For example, this is the order in which keys will now be tried for a `blank`
+ error on a `product`'s `title` attribute with current locale set to `en-US`:
+
+ en-US.activerecord.errors.models.product.attributes.title.blank
+ en-US.activerecord.errors.models.product.blank
+ en-US.activerecord.errors.messages.blank
+
+ en.activerecord.errors.models.product.attributes.title.blank
+ en.activerecord.errors.models.product.blank
+ en.activerecord.errors.messages.blank
+
+ en-US.errors.attributes.title.blank
+ en-US.errors.messages.blank
+
+ en.errors.attributes.title.blank
+ en.errors.messages.blank
+
+ *Hugo Vacher*
+
+
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta2 (February 25, 2019) ##
+
+* Fix date value when casting a multiparameter date hash to not convert
+ from Gregorian date to Julian date.
+
+ Before:
+
+ Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
+ # => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
+
+ After:
+
+ Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
+ # => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
+
+ Fixes #28521.
+
+ *Sayan Chakraborty*
+
+* Fix year value when casting a multiparameter time hash.
+
+ When assigning a hash to a time attribute that's missing a year component
+ (e.g. a `time_select` with `:ignore_date` set to `true`) then the year
+ defaults to 1970 instead of the expected 2000. This results in the attribute
+ changing as a result of the save.
+
+ Before:
+ ```
+ event = Event.new(start_time: { 4 => 20, 5 => 30 })
+ event.start_time # => 1970-01-01 20:30:00 UTC
+ event.save
+ event.reload
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ ```
+
+ After:
+ ```
+ event = Event.new(start_time: { 4 => 20, 5 => 30 })
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ event.save
+ event.reload
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ ```
+
+ *Andrew White*
+
+
+## Rails 6.0.0.beta1 (January 18, 2019) ##
+
+* Add `ActiveModel::Errors#of_kind?`.
+
+ *bogdanvlviv*, *Rafael Mendonça França*
+
+* Fix numericality equality validation of `BigDecimal` and `Float`
+ by casting to `BigDecimal` on both ends of the validation.
+
+ *Gannon McGibbon*
+
+* Add `#slice!` method to `ActiveModel::Errors`.
+
+ *Daniel López Prat*
+
+* Fix numericality validator to still use value before type cast except Active Record.
+
+ Fixes #33651, #33686.
+
+ *Ryuta Kamizono*
+
+* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps.
+
+ Before:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC
+ ```
+
+ After:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z"
+ ```
+
+ *Bogdan Gusiev*
+
* Allows configurable attribute name for `#has_secure_password`. This
still defaults to an attribute named 'password', causing no breaking
change. There is a new method `#authenticate_XXX` where XXX is the
configured attribute name, making the existing `#authenticate` now an
alias for this when the attribute is the default 'password'.
+
Example:
class User < ActiveRecord::Base
@@ -14,17 +134,17 @@
user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..."
user.authenticate_recovery_password('42password') # => user
- *Unathi Chonco*
+ *Unathi Chonco*
-* Add `config.active_model.i18n_full_message` in order to control whether
+* Add `config.active_model.i18n_customize_full_message` in order to control whether
the `full_message` error format can be overridden at the attribute or model
level in the locale files. This is `false` by default.
*Martin Larochelle*
-* Rails 6 requires Ruby 2.4.1 or newer.
+* Rails 6 requires Ruby 2.5.0 or newer.
- *Jeremy Daer*
+ *Jeremy Daer*, *Kasper Timm Hansen*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 1cb3add0fc..ab7c27c209 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2018 David Heinemeier Hansson
+Copyright (c) 2004-2019 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index 1aaf4813ea..f9370d8de0 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -5,6 +5,8 @@ They allow for Action Pack helpers to interact with non-Active Record models,
for example. Active Model also helps with building custom ORMs for use outside of
the Rails framework.
+You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide.
+
Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
interact with Action Pack helpers, it was required to either copy chunks of
code from Rails, or monkey patch entire helpers to make them handle objects
@@ -253,7 +255,7 @@ Active Model is released under the MIT license:
API documentation is at:
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
Bug reports for the Ruby on Rails project can be filed here:
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 7be466dc4c..e681654439 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -9,13 +9,13 @@ Gem::Specification.new do |s|
s.summary = "A toolkit for building modeling frameworks (part of Rails)."
s.description = "A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing."
- s.required_ruby_version = ">= 2.4.1"
+ s.required_ruby_version = ">= 2.5.0"
s.license = "MIT"
s.author = "David Heinemeier Hansson"
s.email = "david@loudthinking.com"
- s.homepage = "http://rubyonrails.org"
+ s.homepage = "https://rubyonrails.org"
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"]
s.require_path = "lib"
@@ -25,5 +25,8 @@ Gem::Specification.new do |s|
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md"
}
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
+
s.add_dependency "activesupport", version
end
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index bc10d6b4b9..c9140dc582 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2018 David Heinemeier Hansson
+# Copyright (c) 2004-2019 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb
index 3f19cda07b..75f60d205e 100644
--- a/activemodel/lib/active_model/attribute.rb
+++ b/activemodel/lib/active_model/attribute.rb
@@ -206,6 +206,7 @@ module ActiveModel
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:
diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb
index 217bf1ac01..f0e3458f51 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -27,7 +27,7 @@ module ActiveModel
# 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.empty?
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 888a431e5f..5c4670f393 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -369,7 +369,7 @@ module ActiveModel
"define_method(:'#{name}') do |*args|"
end
- extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
target = if CALL_COMPILABLE_REGEXP.match?(send)
"#{"self." unless include_private}#{send}(#{extra})"
@@ -474,5 +474,43 @@ module ActiveModel
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.alias_method method_name, temp_method_name
+ mod.undef_method temp_method_name
+ end
+ end
+ end
end
end
diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb
index a890ee3932..4679b33852 100644
--- a/activemodel/lib/active_model/attribute_set.rb
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -37,16 +37,8 @@ module ActiveModel
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
+ def fetch_value(name, &block)
+ self[name].value(&block)
end
def write_from_database(name, value)
diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb
index 5bf213d593..c3a446098c 100644
--- a/activemodel/lib/active_model/attributes.rb
+++ b/activemodel/lib/active_model/attributes.rb
@@ -29,17 +29,16 @@ module ActiveModel
private
def define_method_attribute=(name)
- safe_name = name.unpack1("h*".freeze)
- ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name
-
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name}
- write_attribute(name, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ 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:
@@ -97,15 +96,4 @@ module ActiveModel
write_attribute(attribute_name, value)
end
end
-
- module AttributeMethods #:nodoc:
- AttrNames = Module.new {
- def self.set_name_cache(name, value)
- const_name = "ATTR_#{name}"
- unless const_defined? const_name
- const_set const_name, -value
- end
- end
- }
- end
end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index fde3381df2..ea2ed7dff7 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
module ActiveModel
# == Active \Model \Callbacks
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index cdc1282817..82713ddc81 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -103,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 093052a70c..0d9e761b1e 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -141,7 +141,9 @@ module ActiveModel
@mutations_from_database = nil
end
- def changes_applied # :nodoc:
+ # 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
@@ -151,7 +153,7 @@ module ActiveModel
@mutations_from_database = nil
end
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
+ # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
#
# person.changed? # => false
# person.name = 'bob'
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index edc30ee64d..3a692a3e64 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -63,9 +63,9 @@ module ActiveModel
MESSAGE_OPTIONS = [:message]
class << self
- attr_accessor :i18n_full_message # :nodoc:
+ attr_accessor :i18n_customize_full_message # :nodoc:
end
- self.i18n_full_message = false
+ self.i18n_customize_full_message = false
attr_reader :messages, :details
@@ -112,6 +112,17 @@ module ActiveModel
@details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
end
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
+ #
+ # person.errors.keys # => [:name, :age, :gender, :city]
+ # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
+ # person.errors.keys # => [:age, :gender]
+ def slice!(*keys)
+ keys = keys.map(&:to_sym)
+ @details.slice!(*keys)
+ @messages.slice!(*keys)
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
@@ -317,21 +328,42 @@ module ActiveModel
# 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.
+ # If the error message requires options, then it returns +true+ with
+ # the correct options, or +false+ with incorrect or missing options.
#
- # 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
+ # 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)
+
if message.is_a? Symbol
- self.details[attribute.to_sym].map { |e| e[:error] }.include? message
+ details[attribute.to_sym].include? normalize_detail(message, options)
+ else
+ self[attribute].include? message
+ end
+ end
+
+ # Returns +true+ if an error on the attribute with the given message is
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
+ #
+ # person.errors.add :age
+ # person.errors.add :name, :too_long, { count: 25 }
+ # person.errors.of_kind? :age # => true
+ # person.errors.of_kind? :name # => false
+ # person.errors.of_kind? :name, :too_long # => true
+ # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
+ # person.errors.of_kind? :name, :not_too_long # => false
+ # person.errors.of_kind? :name, "is too long" # => false
+ def of_kind?(attribute, message = :invalid)
+ message = message.call if message.respond_to?(:call)
+
+ if message.is_a? Symbol
+ details[attribute.to_sym].map { |e| e[:error] }.include? message
else
- message = message.call if message.respond_to?(:call)
- message = normalize_message(attribute, message, options)
self[attribute].include? message
end
end
@@ -379,9 +411,11 @@ module ActiveModel
# * <tt>errors.format</tt>
def full_message(attribute, message)
return message if attribute == :base
+ attribute = attribute.to_s
- if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope)
- parts = attribute.to_s.split(".")
+ if self.class.i18n_customize_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"
@@ -410,8 +444,9 @@ module ActiveModel
defaults << :"errors.format"
defaults << "%{attribute} %{message}"
- attr_name = attribute.to_s.tr(".", "_").humanize
+ attr_name = attribute.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+
I18n.t(defaults.shift,
default: defaults,
attribute: attr_name,
@@ -444,6 +479,14 @@ module ActiveModel
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
+ value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
+
+ options = {
+ model: @base.model_name.human,
+ attribute: @base.class.human_attribute_name(attribute),
+ value: value,
+ object: @base
+ }.merge!(options)
if @base.class.respond_to?(:i18n_scope)
i18n_scope = @base.class.i18n_scope.to_s
@@ -452,6 +495,11 @@ module ActiveModel
:"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
defaults << :"#{i18n_scope}.errors.messages.#{type}"
+
+ catch(:exception) do
+ translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
+ return translation unless translation.nil?
+ end unless options[:message]
else
defaults = []
end
@@ -461,15 +509,7 @@ module ActiveModel
key = defaults.shift
defaults = options.delete(:message) if options[:message]
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
-
- options = {
- default: defaults,
- model: @base.model_name.human,
- attribute: @base.class.human_attribute_name(attribute),
- value: value,
- object: @base
- }.merge!(options)
+ options[:default] = defaults
I18n.translate(key, options)
end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index cef5441e4a..1626aac468 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -10,7 +10,7 @@ module ActiveModel
MAJOR = 6
MINOR = 0
TINY = 0
- PRE = "alpha"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index dfccd03cd8..bf23fa3c05 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -111,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:
@@ -131,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+
@@ -193,7 +209,7 @@ module ActiveModel
private
def _singularize(string)
- ActiveSupport::Inflector.underscore(string).tr("/".freeze, "_".freeze)
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
end
@@ -236,7 +252,7 @@ module ActiveModel
# Person.model_name.plural # => "people"
def model_name
@_model_name ||= begin
- namespace = 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 0ed70bd473..eb7901c7e9 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -13,8 +13,8 @@ module ActiveModel
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
+ initializer "active_model.i18n_customize_full_message" do
+ ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_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 51d54f34f3..5f409326bd 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -69,6 +69,27 @@ module ActiveModel
raise
end
+ include InstanceMethodsOnActivation.new(attribute)
+
+ if validations
+ include ActiveModel::Validations
+
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # 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(attribute, :blank) unless record.send("#{attribute}_digest").present?
+ end
+
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
+ end
+ end
+ end
+
+ class InstanceMethodsOnActivation < Module
+ def initialize(attribute)
attr_reader attribute
define_method("#{attribute}=") do |unencrypted_password|
@@ -101,21 +122,6 @@ module ActiveModel
end
alias_method :authenticate, :authenticate_password if attribute == :password
-
- if validations
- include ActiveModel::Validations
-
- # This ensures the model has a password by checking whether the password_digest
- # is present, so that this works with both new and existing records. However,
- # 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(attribute, :blank) unless record.send("#{attribute}_digest").present?
- end
-
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of attribute, allow_blank: true
- end
end
end
end
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index 25e1541d66..f77fb98c32 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -26,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:
@@ -40,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.
@@ -48,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.
@@ -63,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" } ] }
#
@@ -81,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" } ],
@@ -93,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
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f6c6efbc87..e64d2c793c 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -14,7 +14,16 @@ module ActiveModel
# - 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
+ FALSE_VALUES = [
+ false, 0,
+ "0", :"0",
+ "f", :f,
+ "F", :F,
+ "false", :false,
+ "FALSE", :FALSE,
+ "off", :off,
+ "OFF", :OFF,
+ ].to_set.freeze
def type # :nodoc:
:boolean
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 8ec5deedc4..c5fe926039 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -3,16 +3,13 @@
module ActiveModel
module Type
class Date < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::AcceptsMultiparameterTime.new
def type
:date
end
- def serialize(value)
- cast(value)
- end
-
def type_cast_for_schema(value)
value.to_s(:db).inspect
end
@@ -49,7 +46,7 @@ module ActiveModel
def value_from_multiparameter_assignment(*)
time = super
- time && time.to_date
+ time && new_date(time.year, time.mon, time.mday)
end
end
end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index 9641bf45ee..133410e821 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -3,6 +3,7 @@
module ActiveModel
module Type
class DateTime < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
defaults: { 4 => 0, 5 => 0 }
@@ -12,10 +13,6 @@ module ActiveModel
:datetime
end
- def serialize(value)
- super(cast(value))
- end
-
private
def cast_value(value)
@@ -39,9 +36,9 @@ module ActiveModel
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
+ 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
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 9dbe32e5a6..ea1987df7c 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -18,8 +18,6 @@ module ActiveModel
end
end
- alias serialize cast
-
private
def cast_value(value)
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index 403f0a9e6b..20145d5f0d 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -4,3 +4,4 @@ require "active_model/type/helpers/accepts_multiparameter_time"
require "active_model/type/helpers/numeric"
require "active_model/type/helpers/mutable"
require "active_model/type/helpers/time_value"
+require "active_model/type/helpers/timezone"
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 ad891f841e..e15d7b013f 100644
--- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -5,6 +5,10 @@ module ActiveModel
module Helpers # :nodoc: all
class AcceptsMultiparameterTime < Module
def initialize(defaults: {})
+ define_method(:serialize) do |value|
+ super(cast(value))
+ end
+
define_method(:cast) do |value|
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index 16e14f9e5f..1d8171e25b 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -4,6 +4,10 @@ module ActiveModel
module Type
module Helpers # :nodoc: all
module Numeric
+ def serialize(value)
+ cast(value)
+ end
+
def cast(value)
value = \
case value
@@ -22,15 +26,18 @@ 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)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
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/
+ !NUMERIC_REGEX.match?(value)
end
+
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
+ private_constant :NUMERIC_REGEX
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 cb6aa67a9d..735b9a75a6 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -21,18 +21,6 @@ module ActiveModel
value
end
- def is_utc?
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
- end
-
- def default_timezone
- if is_utc?
- :utc
- else
- :local
- end
- end
-
def apply_seconds_precision(value)
return value unless precision && value.respond_to?(:usec)
number_of_insignificant_digits = 6 - precision
@@ -70,7 +58,13 @@ module ActiveModel
# Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
+ 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
diff --git a/activemodel/lib/active_model/type/helpers/timezone.rb b/activemodel/lib/active_model/type/helpers/timezone.rb
new file mode 100644
index 0000000000..cf87b9715b
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/timezone.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module Timezone
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
+ end
+
+ def default_timezone
+ is_utc? ? :utc : :local
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index da74aaa3c5..1e1061ff60 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -18,35 +18,23 @@ module ActiveModel
:integer
end
- def deserialize(value)
- return if value.nil?
- value.to_i
- end
-
def serialize(value)
- result = cast(value)
- if result
- ensure_in_range(result)
- end
- result
+ return if value.is_a?(::String) && non_numeric_string?(value)
+ ensure_in_range(super)
end
private
attr_reader :range
def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
- value.to_i rescue nil
- end
+ value.to_i rescue nil
end
def ensure_in_range(value)
- unless range.cover?(value)
+ if value && !range.cover?(value)
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
end
+ value
end
def max_value
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 36f13945b1..a9c9bfadb6 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -16,8 +16,8 @@ module ActiveModel
def cast_value(value)
case value
when ::String then ::String.new(value)
- when true then "t".freeze
- when false then "f".freeze
+ when true then "t"
+ when false then "f"
else value.to_s
end
end
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index b3056b1333..61847a4ce7 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -3,9 +3,10 @@
module ActiveModel
module Type
class Time < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
)
def type
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 7f14d102dd..f18f9a601a 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
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
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index ea3a6b52ab..6fd54270f2 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -54,8 +54,9 @@ module ActiveModel
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)
+ klass.define_attribute_methods
+ klass.attr_reader(*attr_readers)
+ klass.attr_writer(*attr_writers)
end
private
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 1b5d5b09ab..b549755ba4 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -19,11 +19,11 @@ module ActiveModel
private
def setup!(klass)
- klass.send(:attr_reader, *attributes.map do |attribute|
+ klass.attr_reader(*attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
end.compact)
- klass.send(:attr_writer, *attributes.map do |attribute|
+ klass.attr_writer(*attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
end.compact)
end
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index d6c80b2c5d..02759b4ccb 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -32,7 +32,7 @@ module ActiveModel
value = options[key]
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"
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
end
end
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 0478915be7..51e224d5cd 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "bigdecimal/util"
+
module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
@@ -9,6 +11,8 @@ module ActiveModel
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
+ INTEGER_REGEX = /\A[+-]?\d+\z/
+
def check_validity!
keys = CHECKS.keys - [:odd, :even]
options.slice(*keys).each do |option, value|
@@ -21,8 +25,17 @@ module ActiveModel
def validate_each(record, attr_name, value)
came_from_user = :"#{attr_name}_came_from_user?"
- if record.respond_to?(came_from_user) && record.public_send(came_from_user)
- raw_value = record.read_attribute_before_type_cast(attr_name)
+ 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
@@ -40,11 +53,7 @@ module ActiveModel
return
end
- if raw_value.is_a?(Numeric)
- value = raw_value
- else
- value = parse_raw_value_as_a_number(raw_value)
- end
+ value = parse_as_number(raw_value)
options.slice(*CHECKS.keys).each do |option, option_value|
case option
@@ -60,6 +69,8 @@ module ActiveModel
option_value = record.send(option_value)
end
+ option_value = parse_as_number(option_value)
+
unless value.send(CHECKS[option], option_value)
record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
end
@@ -70,18 +81,29 @@ module ActiveModel
private
def is_number?(raw_value)
- !parse_raw_value_as_a_number(raw_value).nil?
+ !parse_as_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]/
+ def parse_as_number(raw_value)
+ if raw_value.is_a?(Float)
+ raw_value.to_d
+ elsif raw_value.is_a?(Numeric)
+ raw_value
+ elsif is_integer?(raw_value)
+ raw_value.to_i
+ elsif !is_hexadecimal_literal?(raw_value)
+ Kernel.Float(raw_value).to_d
+ end
end
def is_integer?(raw_value)
- /\A[+-]?\d+\z/ === raw_value.to_s
+ INTEGER_REGEX.match?(raw_value.to_s)
+ end
+
+ def is_hexadecimal_literal?(raw_value)
+ /\A0[xX]/.match?(raw_value.to_s)
end
def filtered_options(value)
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 88cca318ef..21c4ce0dfe 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -116,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
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index e17c3ca7b3..94d53b8dd1 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -90,7 +90,7 @@ module ActiveModel
# class MyValidator < ActiveModel::Validator
# def initialize(options={})
# super
- # options[:class].send :attr_accessor, :custom_attribute
+ # options[:class].attr_accessor :custom_attribute
# end
# end
class Validator
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index b06291f1b4..30e8419685 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -100,9 +100,11 @@ class AttributeAssignmentTest < ActiveModel::TestCase
end
test "an ArgumentError is raised if a non-hash-like object is passed" do
- assert_raises(ArgumentError) do
+ err = assert_raises(ArgumentError) do
Model.new(1)
end
+
+ assert_equal("When assigning attributes, you must pass a hash as an argument, Integer passed.", err.message)
end
test "forbidden attributes cannot be used for mass assignment" do
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index 0cfc6f4b6b..ebb6cc542d 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -106,14 +106,12 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test "#define_attribute_method generates attribute method" do
- begin
- ModelWithAttributes.define_attribute_method(:foo)
+ ModelWithAttributes.define_attribute_method(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
end
test "#define_attribute_method does not generate attribute method if already defined in attribute module" do
@@ -140,36 +138,30 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test "#define_attribute_method generates attribute method with invalid identifier characters" do
- begin
- ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
+ ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
- assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
- assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b")
- ensure
- ModelWithWeirdNamesAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b")
+ ensure
+ ModelWithWeirdNamesAttributes.undefine_attribute_methods
end
test "#define_attribute_methods works passing multiple arguments" do
- begin
- ModelWithAttributes.define_attribute_methods(:foo, :baz)
+ ModelWithAttributes.define_attribute_methods(:foo, :baz)
- assert_equal "value of foo", ModelWithAttributes.new.foo
- assert_equal "value of baz", ModelWithAttributes.new.baz
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_equal "value of baz", ModelWithAttributes.new.baz
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
end
test "#define_attribute_methods generates attribute methods" do
- begin
- ModelWithAttributes.define_attribute_methods(:foo)
+ ModelWithAttributes.define_attribute_methods(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
end
test "#alias_attribute generates attribute_aliases lookup hash" do
@@ -182,38 +174,32 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test "#define_attribute_methods generates attribute methods with spaces in their names" do
- begin
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
- ensure
- ModelWithAttributesWithSpaces.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
end
test "#alias_attribute works with attributes with spaces in their names" do
- begin
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
- ensure
- ModelWithAttributesWithSpaces.undefine_attribute_methods
- end
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
end
test "#alias_attribute works with attributes named as a ruby keyword" do
- begin
- ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
-
- assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
- assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
- ensure
- ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
- end
+ ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
+
+ assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
+ assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ ensure
+ ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
end
test "#undefine_attribute_methods removes attribute methods" do
diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb
index b868dba743..62feb9074e 100644
--- a/activemodel/test/cases/attribute_set_test.rb
+++ b/activemodel/test/cases/attribute_set_test.rb
@@ -217,7 +217,7 @@ module ActiveModel
assert_equal({ foo: "1" }, attributes.to_hash)
end
- test "marshaling dump/load legacy materialized attribute hash" do
+ test "marshalling dump/load legacy materialized attribute hash" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
attributes = builder.build_from_database(foo: "1")
diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb
index ea2b0efd11..097db2e923 100644
--- a/activemodel/test/cases/attribute_test.rb
+++ b/activemodel/test/cases/attribute_test.rb
@@ -78,7 +78,7 @@ module ActiveModel
end
test "duping dups the value" do
- @type.expect(:deserialize, "type cast".dup, ["a value"])
+ @type.expect(:deserialize, +"type cast", ["a value"])
attribute = Attribute.from_database(nil, "a value", @type)
value_from_orig = attribute.value
@@ -204,7 +204,7 @@ module ActiveModel
assert_not_predicate unchanged, :changed?
end
- test "an attribute can not be mutated if it has not been read,
+ test "an attribute cannot be mutated if it has not been read,
and skips expensive calculations" do
type_which_raises_from_all_methods = Object.new
attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
@@ -246,7 +246,7 @@ module ActiveModel
end
test "with_type preserves mutations" do
- attribute = Attribute.from_database(:foo, "".dup, Type::Value.new)
+ attribute = Attribute.from_database(:foo, +"", Type::Value.new)
attribute.value << "1"
assert_equal 1, attribute.with_type(Type::Integer.new).value
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index 1ec12d8222..0711dc56ca 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -112,7 +112,7 @@ class CallbacksTest < ActiveModel::TestCase
def callback1; history << "callback1"; end
def callback2; history << "callback2"; end
def create
- run_callbacks(:create) {}
+ run_callbacks(:create) { }
self
end
end
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index b120e68027..0edbbffa86 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -14,37 +14,23 @@ class DirtyTest < ActiveModel::TestCase
@status = "initialized"
end
- def name
- @name
- end
+ attr_reader :name, :color, :size, :status
def name=(val)
name_will_change!
@name = val
end
- def color
- @color
- end
-
def color=(val)
color_will_change! unless val == @color
@color = val
end
- def size
- @size
- end
-
def size=(val)
attribute_will_change!(:size) unless val == @size
@size = val
end
- def status
- @status
- end
-
def status=(val)
status_will_change! unless val == @status
@status = val
@@ -108,7 +94,7 @@ class DirtyTest < ActiveModel::TestCase
end
test "attribute mutation" do
- @model.instance_variable_set("@name", "Yam".dup)
+ @model.instance_variable_set("@name", +"Yam")
assert_not_predicate @model, :name_changed?
@model.name.replace("Hadad")
assert_not_predicate @model, :name_changed?
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 41ff6443fe..947f9bf99b 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -209,6 +209,8 @@ class ErrorsTest < ActiveModel::TestCase
person.errors.add(:name, "cannot be blank")
person.errors.add(:name, "is invalid")
assert person.errors.added?(:name, "cannot be blank")
+ assert person.errors.added?(:name, "is invalid")
+ assert_not person.errors.added?(:name, "incorrect")
end
test "added? returns false when no errors are present" do
@@ -222,17 +224,103 @@ class ErrorsTest < ActiveModel::TestCase
assert_not person.errors.added?(:name, "cannot be blank")
end
- test "added? returns false when checking for an error, but not providing message arguments" do
+ test "added? returns false when checking for an error, but not providing message argument" do
person = Person.new
person.errors.add(:name, "cannot be blank")
assert_not person.errors.added?(:name)
end
+ test "added? returns false when checking for an error with an incorrect or missing option" do
+ person = Person.new
+ person.errors.add :name, :too_long, count: 25
+
+ assert person.errors.added? :name, :too_long, count: 25
+ assert person.errors.added? :name, "is too long (maximum is 25 characters)"
+ assert_not person.errors.added? :name, :too_long, count: 24
+ assert_not person.errors.added? :name, :too_long
+ assert_not person.errors.added? :name, "is too long"
+ end
+
test "added? returns false when checking for an error by symbol and a different error with same message is present" do
I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } })
person = Person.new
person.errors.add(:name, :wrong)
assert_not person.errors.added?(:name, :used)
+ assert person.errors.added?(:name, :wrong)
+ end
+
+ test "of_kind? returns false when checking for an error, but not providing message argument" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_not person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? returns false when checking a nonexisting error and other errors are present for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "is invalid")
+ assert_not person.errors.of_kind?(:name, "cannot be blank")
+ end
+
+ test "of_kind? returns false when no errors are present" do
+ person = Person.new
+ assert_not person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? matches the given message when several errors are present for the same attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "is invalid")
+ assert person.errors.of_kind?(:name, "cannot be blank")
+ assert person.errors.of_kind?(:name, "is invalid")
+ assert_not person.errors.of_kind?(:name, "incorrect")
+ end
+
+ test "of_kind? defaults message to :invalid" do
+ person = Person.new
+ person.errors.add(:name)
+ assert person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? handles proc messages" do
+ person = Person.new
+ message = Proc.new { "cannot be blank" }
+ person.errors.add(:name, message)
+ assert person.errors.of_kind?(:name, message)
+ end
+
+ test "of_kind? returns true when string attribute is used with a symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.of_kind?("name", :blank)
+ end
+
+ test "of_kind? handles symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.of_kind?(:name, :blank)
+ end
+
+ test "of_kind? detects indifferent if a specific error was added to the object" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert person.errors.of_kind?(:name, "cannot be blank")
+ assert person.errors.of_kind?("name", "cannot be blank")
+ end
+
+ test "of_kind? ignores options" do
+ person = Person.new
+ person.errors.add :name, :too_long, count: 25
+
+ assert person.errors.of_kind? :name, :too_long
+ assert person.errors.of_kind? :name, "is too long (maximum is 25 characters)"
+ end
+
+ test "of_kind? returns false when checking for an error by symbol and a different error with same message is present" do
+ I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } })
+ person = Person.new
+ person.errors.add(:name, :wrong)
+ assert_not person.errors.of_kind?(:name, :used)
+ assert person.errors.of_kind?(:name, :wrong)
end
test "size calculates the number of error messages" do
@@ -401,6 +489,30 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
end
+ test "slice! removes all errors except the given keys" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ person.errors.slice!(:age, "gender")
+
+ assert_equal [:age, :gender], person.errors.keys
+ end
+
+ test "slice! returns the deleted errors" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ removed_errors = person.errors.slice!(:age, "gender")
+
+ assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors)
+ end
+
test "errors are marshalable" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 91fb9d0a7c..a4cb472ffc 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -14,12 +14,16 @@ require "active_support/testing/method_call_assertions"
class ActiveModel::TestCase < ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
- # Skips the current run on Rubinius using Minitest::Assertions#skip
- private def rubinius_skip(message = "")
- skip message if RUBY_ENGINE == "rbx"
- end
- # Skips the current run on JRuby using Minitest::Assertions#skip
- private def jruby_skip(message = "")
- skip message if defined?(JRUBY_VERSION)
- end
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
+ end
+
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
+ end
end
+
+require_relative "../../../tools/test_common"
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index ab60285e2a..95ee7cace3 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -32,23 +32,23 @@ class RailtieTest < ActiveModel::TestCase
assert_equal true, ActiveModel::SecurePassword.min_cost
end
- test "i18n full message defaults to false" do
+ test "i18n customize full message defaults to false" do
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_full_message
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
end
- test "i18n full message can be disabled" do
- @app.config.active_model.i18n_full_message = false
+ test "i18n customize full message can be disabled" do
+ @app.config.active_model.i18n_customize_full_message = false
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_full_message
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
end
- test "i18n full message can be enabled" do
- @app.config.active_model.i18n_full_message = true
+ test "i18n customize full message can be enabled" do
+ @app.config.active_model.i18n_customize_full_message = true
@app.initialize!
- assert_equal true, ActiveModel::Errors.i18n_full_message
+ assert_equal true, ActiveModel::Errors.i18n_customize_full_message
end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 9ef1148be8..0aca714bd2 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -184,6 +184,20 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_nil @existing_user.password_digest
end
+ test "override secure password attribute" do
+ assert_nil @user.password_called
+
+ @user.password = "secret"
+
+ assert_equal "secret", @user.password
+ assert_equal 1, @user.password_called
+
+ @user.password = "terces"
+
+ assert_equal "terces", @user.password
+ assert_equal 2, @user.password_called
+ end
+
test "authenticate" do
@user.password = "secret"
@user.recovery_password = "42password"
@@ -206,16 +220,14 @@ class SecurePasswordTest < ActiveModel::TestCase
end
test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
- begin
- original_bcrypt_cost = BCrypt::Engine.cost
- ActiveModel::SecurePassword.min_cost = false
- BCrypt::Engine.cost = 5
-
- @user.password = "secret"
- assert_equal BCrypt::Engine.cost, @user.password_digest.cost
- ensure
- BCrypt::Engine.cost = original_bcrypt_cost
- end
+ original_bcrypt_cost = BCrypt::Engine.cost
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ ensure
+ BCrypt::Engine.cost = original_bcrypt_cost
end
test "Password digest cost can be set to bcrypt min cost to speed up tests" do
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index aae98c9fe4..84efc8de0d 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -26,20 +26,18 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "should include root in json if include_root_in_json is true" do
- begin
- original_include_root_in_json = Contact.include_root_in_json
- Contact.include_root_in_json = true
- json = @contact.to_json
-
- assert_match %r{^\{"contact":\{}, json
- assert_match %r{"name":"Konata Izumi"}, json
- assert_match %r{"age":16}, json
- assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
- assert_match %r{"awesome":true}, json
- assert_match %r{"preferences":\{"shows":"anime"\}}, json
- ensure
- Contact.include_root_in_json = original_include_root_in_json
- end
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.to_json
+
+ assert_match %r{^\{"contact":\{}, json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
+ assert_match %r{"awesome":true}, json
+ assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
end
test "should include root in json (option) even if the default is set to false" do
@@ -129,20 +127,22 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_equal :name, options[:except]
end
+ test "as_json should serialize timestamps" do
+ assert_equal "2006-08-01T00:00:00.000Z", @contact.as_json["created_at"]
+ end
+
test "as_json should return a hash if include_root_in_json is true" do
- begin
- original_include_root_in_json = Contact.include_root_in_json
- Contact.include_root_in_json = true
- json = @contact.as_json
-
- assert_kind_of Hash, json
- assert_kind_of Hash, json["contact"]
- %w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json["contact"][field]
- end
- ensure
- Contact.include_root_in_json = original_include_root_in_json
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.as_json
+
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json["contact"]
+ %w(name age created_at awesome preferences).each do |field|
+ assert_equal @contact.send(field).as_json, json["contact"][field]
end
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
end
test "from_json should work without a root (class attribute)" do
diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb
index 2de0f53640..7f8490b2fe 100644
--- a/activemodel/test/cases/type/boolean_test.rb
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -23,6 +23,13 @@ module ActiveModel
assert type.cast("\u3000\r\n")
assert type.cast("\u0000")
assert type.cast("SOMETHING RANDOM")
+ assert type.cast(:"1")
+ assert type.cast(:t)
+ assert type.cast(:T)
+ assert type.cast(:true)
+ assert type.cast(:TRUE)
+ assert type.cast(:on)
+ assert type.cast(:ON)
# explicitly check for false vs nil
assert_equal false, type.cast(false)
@@ -34,6 +41,13 @@ module ActiveModel
assert_equal false, type.cast("FALSE")
assert_equal false, type.cast("off")
assert_equal false, type.cast("OFF")
+ assert_equal false, type.cast(:"0")
+ assert_equal false, type.cast(:f)
+ assert_equal false, type.cast(:F)
+ assert_equal false, type.cast(:false)
+ assert_equal false, type.cast(:FALSE)
+ assert_equal false, type.cast(:off)
+ assert_equal false, type.cast(:OFF)
end
end
end
diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb
index e8cf178612..2dd1a55616 100644
--- a/activemodel/test/cases/type/date_test.rb
+++ b/activemodel/test/cases/type/date_test.rb
@@ -12,8 +12,22 @@ module ActiveModel
assert_nil type.cast(" ")
assert_nil type.cast("ABC")
- date_string = ::Time.now.utc.strftime("%F")
+ now = ::Time.now.utc
+ values_hash = { 1 => now.year, 2 => now.mon, 3 => now.mday }
+ date_string = now.strftime("%F")
assert_equal date_string, type.cast(date_string).strftime("%F")
+ assert_equal date_string, type.cast(values_hash).strftime("%F")
+ end
+
+ def test_returns_correct_year
+ type = Type::Date.new
+
+ time = ::Time.utc(1, 1, 1)
+ date = ::Date.new(time.year, time.mon, time.mday)
+
+ values_hash_for_multiparameter_assignment = { 1 => 1, 2 => 1, 3 => 1 }
+
+ assert_equal date, type.cast(values_hash_for_multiparameter_assignment)
end
end
end
diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb
index 60f62becc2..74b47d1b4d 100644
--- a/activemodel/test/cases/type/date_time_test.rb
+++ b/activemodel/test/cases/type/date_time_test.rb
@@ -25,6 +25,17 @@ module ActiveModel
end
end
+ def test_hash_to_time
+ type = Type::DateTime.new
+ assert_equal ::Time.utc(2018, 10, 15, 0, 0, 0), type.cast(1 => 2018, 2 => 10, 3 => 15)
+ end
+
+ def test_hash_with_wrong_keys
+ type = Type::DateTime.new
+ error = assert_raises(ArgumentError) { type.cast(a: 1) }
+ assert_equal "Provided hash {:a=>1} doesn't contain necessary keys: [1, 2, 3]", error.message
+ end
+
private
def with_timezone_config(default:)
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index c0cf6ce590..be60c4f7fa 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -57,9 +57,12 @@ module ActiveModel
def test_changed?
type = Decimal.new
- assert type.changed?(5.0, 5.0, "5.0wibble")
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5.0wibble")
assert_not type.changed?(5.0, 5.0, "5.0")
assert_not type.changed?(-5.0, -5.0, "-5.0")
+ assert_not type.changed?(5.0, 5.0, "0.5e+1")
end
def test_scale_is_applied_before_precision_to_prevent_rounding_errors
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
index 28318e06f8..230a8dda32 100644
--- a/activemodel/test/cases/type/float_test.rb
+++ b/activemodel/test/cases/type/float_test.rb
@@ -21,9 +21,12 @@ module ActiveModel
def test_changing_float
type = Type::Float.new
- assert type.changed?(5.0, 5.0, "5wibble")
+ assert type.changed?(0.0, 0, "wibble")
+ assert type.changed?(5.0, 0, "wibble")
+ assert_not type.changed?(5.0, 5.0, "5wibble")
assert_not type.changed?(5.0, 5.0, "5")
assert_not type.changed?(5.0, 5.0, "5.0")
+ assert_not type.changed?(500.0, 500.0, "0.5E+4")
assert_not type.changed?(nil, nil, nil)
end
end
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
index 8c5d18c9b3..6c02c01237 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -50,12 +50,31 @@ module ActiveModel
assert_equal 7200, type.cast(2.hours)
end
+ test "casting string for database" do
+ type = Type::Integer.new
+ assert_nil type.serialize("wibble")
+ assert_equal 5, type.serialize("5wibble")
+ assert_equal 5, type.serialize(" +5")
+ assert_equal(-5, type.serialize(" -5"))
+ end
+
+ test "casting empty string" do
+ type = Type::Integer.new
+ assert_nil type.cast("")
+ assert_nil type.serialize("")
+ assert_nil type.deserialize("")
+ end
+
test "changed?" do
type = Type::Integer.new
- assert type.changed?(5, 5, "5wibble")
+ assert type.changed?(0, 0, "wibble")
+ assert type.changed?(5, 0, "wibble")
+ assert_not type.changed?(5, 5, "5wibble")
assert_not type.changed?(5, 5, "5")
assert_not type.changed?(5, 5, "5.0")
+ assert_not type.changed?(5, 5, "+5")
+ assert_not type.changed?(5, 5, "+5.0")
assert_not type.changed?(-5, -5, "-5")
assert_not type.changed?(-5, -5, "-5.0")
assert_not type.changed?(nil, nil, nil)
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
index 825c8bb246..9cc530e8db 100644
--- a/activemodel/test/cases/type/string_test.rb
+++ b/activemodel/test/cases/type/string_test.rb
@@ -12,14 +12,22 @@ module ActiveModel
assert_equal "123", type.cast(123)
end
+ test "type casting for database" do
+ type = Type::String.new
+ object, array, hash = Object.new, [true], { a: :b }
+ assert_equal object, type.serialize(object)
+ assert_equal array, type.serialize(array)
+ assert_equal hash, type.serialize(hash)
+ end
+
test "cast strings are mutable" do
type = Type::String.new
- s = "foo".dup
+ s = +"foo"
assert_equal false, type.cast(s).frozen?
assert_equal false, s.frozen?
- f = "foo".freeze
+ f = -"foo"
assert_equal false, type.cast(f).frozen?
assert_equal true, f.frozen?
end
diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb
index 3fbae1a169..5c6271241d 100644
--- a/activemodel/test/cases/type/time_test.rb
+++ b/activemodel/test/cases/type/time_test.rb
@@ -16,6 +16,7 @@ module ActiveModel
assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00")
assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00")
+ assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast(4 => 16, 5 => 45, 6 => 54)
end
def test_user_input_in_time_zone
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 1704db9a48..9674068aff 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -49,7 +49,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
assert_empty t.errors[:title]
end
- def test_unless_validation_using_array_of_true_and_felse_methods
+ def test_unless_validation_using_array_of_true_and_false_methods
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert_predicate t, :valid?
@@ -111,14 +111,14 @@ class ConditionalValidationTest < ActiveModel::TestCase
assert_equal ["hoo 5"], t.errors["title"]
end
- def test_validation_using_conbining_if_true_and_unless_true_conditions
+ def test_validation_using_combining_if_true_and_unless_true_conditions
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert_predicate t, :valid?
assert_empty t.errors[:title]
end
- def test_validation_using_conbining_if_true_and_unless_false_conditions
+ def test_validation_using_combining_if_true_and_unless_false_conditions
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert_predicate t, :invalid?
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index 8603a8ac5c..7bf15e4bee 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -66,24 +66,22 @@ class ConfirmationValidationTest < ActiveModel::TestCase
end
def test_title_confirmation_with_i18n_attribute
- begin
- @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
- I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations("en",
- errors: { messages: { confirmation: "doesn't match %{attribute}" } },
- activemodel: { attributes: { topic: { title: "Test Title" } } })
-
- Topic.validates_confirmation_of(:title)
-
- t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
- assert_predicate t, :invalid?
- assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
- ensure
- I18n.load_path.replace @old_load_path
- I18n.backend = @old_backend
- I18n.backend.reload!
- end
+ @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
+ I18n.load_path.clear
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations("en",
+ errors: { messages: { confirmation: "doesn't match %{attribute}" } },
+ activemodel: { attributes: { topic: { title: "Test Title" } } })
+
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "")
+ assert_predicate t, :invalid?
+ assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
+ ensure
+ I18n.load_path.replace @old_load_path
+ I18n.backend = @old_backend
+ I18n.backend.reload!
end
test "does not override confirmation reader if present" do
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 1c62e750de..eb03e837f1 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -13,8 +13,8 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.backend = I18n::Backend::Simple.new
I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
- @original_i18n_full_message = ActiveModel::Errors.i18n_full_message
- ActiveModel::Errors.i18n_full_message = true
+ @original_i18n_customize_full_message = ActiveModel::Errors.i18n_customize_full_message
+ ActiveModel::Errors.i18n_customize_full_message = true
end
def teardown
@@ -22,7 +22,7 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
- ActiveModel::Errors.i18n_full_message = @original_i18n_full_message
+ ActiveModel::Errors.i18n_customize_full_message = @original_i18n_customize_full_message
end
def test_full_message_encoding
@@ -35,7 +35,7 @@ class I18nValidationTest < ActiveModel::TestCase
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
@person.errors.add(:name, "not found")
- assert_called_with(Person, :human_attribute_name, [:name, default: "Name"], returns: "Person's name") do
+ assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do
assert_equal ["Person's name not found"], @person.errors.full_messages
end
end
@@ -47,7 +47,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_doesnt_use_attribute_format_without_config
- ActiveModel::Errors.i18n_full_message = false
+ ActiveModel::Errors.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
@@ -58,7 +58,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_attribute_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
@@ -69,7 +69,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_model_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { format: "%{message}" } } } })
@@ -80,7 +80,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_deeply_nested_model_attributes_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
@@ -91,7 +91,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_deeply_nested_model_model_format
- ActiveModel::Errors.i18n_full_message = true
+ ActiveModel::Errors.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
@@ -101,6 +101,63 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank")
end
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
+ ActiveModel::Errors.i18n_customize_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
+ ActiveModel::Errors.i18n_customize_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
+
+ person = Person.new
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
+ ActiveModel::Errors.i18n_customize_full_message = true
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts/addresses': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
+ ActiveModel::Errors.i18n_customize_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
+ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
+ ActiveModel::Errors.i18n_customize_full_message = false
+
+ I18n.backend.store_translations("en", activemodel: {
+ attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
+ })
+
+ person = Person.new
+ assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank")
+ assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank")
+ end
+
# ActiveModel::Validations
# A set of common cases for ActiveModel::Validations message generation that
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index 774a2cde74..37e10f783c 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -427,7 +427,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_proc_as_maximum_with_model_method
- Topic.send(:define_method, :max_title_length, lambda { 5 })
+ Topic.define_method(:max_title_length) { 5 }
Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length)
t = Topic.new("title" => "valid", "content" => "whatever")
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 01b78ae72e..16c44762cb 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -66,7 +66,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_with_integer_only_and_proc_as_value
- Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.define_method(:allow_only_integers?) { false }
Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?)
invalid!(NIL + BLANK + JUNK)
@@ -214,23 +214,23 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_proc
- Topic.send(:define_method, :min_approved, lambda { 5 })
+ Topic.define_method(:min_approved) { 5 }
Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved)
invalid!([3, 4])
valid!([5, 6])
ensure
- Topic.send(:remove_method, :min_approved)
+ Topic.remove_method :min_approved
end
def test_validates_numericality_with_symbol
- Topic.send(:define_method, :max_approved, lambda { 5 })
+ Topic.define_method(:max_approved) { 5 }
Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved
invalid!([6])
valid!([4, 5])
ensure
- Topic.send(:remove_method, :max_approved)
+ Topic.remove_method :max_approved
end
def test_validates_numericality_with_numeric_message
@@ -262,6 +262,16 @@ class NumericalityValidationTest < ActiveModel::TestCase
Person.clear_validators!
end
+ def test_validates_numericality_using_value_before_type_cast_if_possible
+ Topic.validates_numericality_of :price
+
+ topic = Topic.new(price: 50)
+
+ assert_equal "$50.00", topic.price
+ assert_equal 50, topic.price_before_type_cast
+ assert_predicate topic, :valid?
+ end
+
def test_validates_numericality_with_exponent_number
base = 10_000_000_000_000_000
Topic.validates_numericality_of :approved, less_than_or_equal_to: base
@@ -271,6 +281,19 @@ class NumericalityValidationTest < ActiveModel::TestCase
assert_predicate topic, :invalid?
end
+ def test_validates_numericality_with_object_acting_as_numeric
+ klass = Class.new do
+ def to_f
+ 123.54
+ end
+ end
+
+ Topic.validates_numericality_of :price
+ topic = Topic.new(price: klass.new)
+
+ assert_predicate topic, :valid?
+ end
+
def test_validates_numericality_with_invalid_args
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" }
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" }
@@ -279,6 +302,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, equal_to: "foo" }
end
+ def test_validates_numericality_equality_for_float_and_big_decimal
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("65.6")
+
+ invalid!([Float("65.5"), BigDecimal("65.7")], "must be equal to 65.6")
+ valid!([Float("65.6"), BigDecimal("65.6")])
+ end
+
private
def invalid!(values, error = nil)
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index b0af00ee45..db3284f833 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -3,6 +3,11 @@
class Topic
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
+ include ActiveModel::AttributeMethods
+ include ActiveSupport::NumberHelper
+
+ attribute_method_suffix "_before_type_cast"
+ define_attribute_method :price
def self._validates_default_keys
super | [ :message ]
@@ -10,6 +15,7 @@ class Topic
attr_accessor :title, :author_name, :content, :approved, :created_at
attr_accessor :after_validation_performed
+ attr_writer :price
after_validation :perform_after_validation
@@ -38,4 +44,12 @@ class Topic
def my_validation_with_arg(attr)
errors.add attr, "is missing" unless send(attr)
end
+
+ def price
+ number_to_currency @price
+ end
+
+ def attribute_before_type_cast(attr)
+ instance_variable_get(:"@#{attr}")
+ end
end
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index bb1b187694..fc4a9e4334 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -10,4 +10,11 @@ class User
has_secure_password :recovery_password, validations: false
attr_accessor :password_digest, :recovery_password_digest
+ attr_accessor :password_called
+
+ def password=(unencrypted_password)
+ self.password_called ||= 0
+ self.password_called += 1
+ super
+ end
end