aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md60
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc6
-rw-r--r--activemodel/Rakefile10
-rw-r--r--activemodel/activemodel.gemspec9
-rwxr-xr-xactivemodel/bin/test3
-rw-r--r--activemodel/lib/active_model.rb9
-rw-r--r--activemodel/lib/active_model/attribute.rb247
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb30
-rw-r--r--activemodel/lib/active_model/attribute_assignment.rb13
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb24
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb116
-rw-r--r--activemodel/lib/active_model/attribute_set.rb113
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb124
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb41
-rw-r--r--activemodel/lib/active_model/attributes.rb108
-rw-r--r--activemodel/lib/active_model/callbacks.rb9
-rw-r--r--activemodel/lib/active_model/conversion.rb2
-rw-r--r--activemodel/lib/active_model/dirty.rb181
-rw-r--r--activemodel/lib/active_model/errors.rb53
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb2
-rw-r--r--activemodel/lib/active_model/gem_version.rb6
-rw-r--r--activemodel/lib/active_model/lint.rb2
-rw-r--r--activemodel/lib/active_model/model.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb8
-rw-r--r--activemodel/lib/active_model/railtie.rb2
-rw-r--r--activemodel/lib/active_model/secure_password.rb8
-rw-r--r--activemodel/lib/active_model/serialization.rb2
-rw-r--r--activemodel/lib/active_model/serializers/json.rb5
-rw-r--r--activemodel/lib/active_model/translation.rb2
-rw-r--r--activemodel/lib/active_model/type.rb18
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb2
-rw-r--r--activemodel/lib/active_model/type/binary.rb2
-rw-r--r--activemodel/lib/active_model/type/boolean.rb2
-rw-r--r--activemodel/lib/active_model/type/date.rb4
-rw-r--r--activemodel/lib/active_model/type/date_time.rb6
-rw-r--r--activemodel/lib/active_model/type/decimal.rb10
-rw-r--r--activemodel/lib/active_model/type/float.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb6
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb4
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb2
-rw-r--r--activemodel/lib/active_model/type/integer.rb6
-rw-r--r--activemodel/lib/active_model/type/registry.rb2
-rw-r--r--activemodel/lib/active_model/type/string.rb9
-rw-r--r--activemodel/lib/active_model/type/time.rb2
-rw-r--r--activemodel/lib/active_model/type/value.rb6
-rw-r--r--activemodel/lib/active_model/validations.rb16
-rw-r--r--activemodel/lib/active_model/validations/absence.rb4
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb4
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb26
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb2
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb6
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/format.rb3
-rw-r--r--activemodel/lib/active_model/validations/helper_methods.rb2
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb4
-rw-r--r--activemodel/lib/active_model/validations/length.rb14
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb21
-rw-r--r--activemodel/lib/active_model/validations/presence.rb3
-rw-r--r--activemodel/lib/active_model/validations/validates.rb9
-rw-r--r--activemodel/lib/active_model/validations/with.rb4
-rw-r--r--activemodel/lib/active_model/validator.rb10
-rw-r--r--activemodel/lib/active_model/version.rb2
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb2
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb4
-rw-r--r--activemodel/test/cases/attribute_set_test.rb257
-rw-r--r--activemodel/test/cases/attribute_test.rb255
-rw-r--r--activemodel/test/cases/attributes_dirty_test.rb205
-rw-r--r--activemodel/test/cases/attributes_test.rb68
-rw-r--r--activemodel/test/cases/callbacks_test.rb2
-rw-r--r--activemodel/test/cases/conversion_test.rb2
-rw-r--r--activemodel/test/cases/dirty_test.rb8
-rw-r--r--activemodel/test/cases/errors_test.rb37
-rw-r--r--activemodel/test/cases/forbidden_attributes_protection_test.rb2
-rw-r--r--activemodel/test/cases/helper.rb2
-rw-r--r--activemodel/test/cases/lint_test.rb2
-rw-r--r--activemodel/test/cases/model_test.rb2
-rw-r--r--activemodel/test/cases/naming_test.rb2
-rw-r--r--activemodel/test/cases/railtie_test.rb2
-rw-r--r--activemodel/test/cases/secure_password_test.rb2
-rw-r--r--activemodel/test/cases/serialization_test.rb2
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb2
-rw-r--r--activemodel/test/cases/translation_test.rb2
-rw-r--r--activemodel/test/cases/type/big_integer_test.rb3
-rw-r--r--activemodel/test/cases/type/binary_test.rb3
-rw-r--r--activemodel/test/cases/type/boolean_test.rb3
-rw-r--r--activemodel/test/cases/type/date_test.rb3
-rw-r--r--activemodel/test/cases/type/date_time_test.rb3
-rw-r--r--activemodel/test/cases/type/decimal_test.rb21
-rw-r--r--activemodel/test/cases/type/float_test.rb11
-rw-r--r--activemodel/test/cases/type/immutable_string_test.rb3
-rw-r--r--activemodel/test/cases/type/integer_test.rb4
-rw-r--r--activemodel/test/cases/type/registry_test.rb3
-rw-r--r--activemodel/test/cases/type/string_test.rb16
-rw-r--r--activemodel/test/cases/type/time_test.rb3
-rw-r--r--activemodel/test/cases/type/value_test.rb3
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb45
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb99
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb15
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb8
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb33
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb41
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/validates_test.rb14
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb2
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb42
-rw-r--r--activemodel/test/cases/validations_test.rb2
-rw-r--r--activemodel/test/models/account.rb2
-rw-r--r--activemodel/test/models/blog_post.rb2
-rw-r--r--activemodel/test/models/contact.rb2
-rw-r--r--activemodel/test/models/custom_reader.rb2
-rw-r--r--activemodel/test/models/helicopter.rb2
-rw-r--r--activemodel/test/models/person.rb6
-rw-r--r--activemodel/test/models/person_with_validator.rb2
-rw-r--r--activemodel/test/models/reply.rb2
-rw-r--r--activemodel/test/models/sheep.rb2
-rw-r--r--activemodel/test/models/topic.rb4
-rw-r--r--activemodel/test/models/track_back.rb2
-rw-r--r--activemodel/test/models/user.rb2
-rw-r--r--activemodel/test/models/visitor.rb2
-rw-r--r--activemodel/test/validators/email_validator.rb1
-rw-r--r--activemodel/test/validators/namespace/email_validator.rb2
131 files changed, 2376 insertions, 335 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index edaac8c7cd..b67a803b9d 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,30 +1,60 @@
-* Remove deprecated behavior that halts callbacks when the return is false.
+* Fix to working before/after validation callbacks on multiple contexts.
- *Rafael Mendonça França*
+ *Yoshiyuki Hirano*
-* Remove unused `ActiveModel::TestCase` class.
+## Rails 5.2.0.beta2 (November 28, 2017) ##
- *Yuji Yaginuma*
+* No changes.
-* Moved DecimalWithoutScale, Text, and UnsignedInteger from Active Model to Active Record
- *Iain Beeston*
+## Rails 5.2.0.beta1 (November 27, 2017) ##
-* Allow indifferent access in `ActiveModel::Errors`.
+* Execute `ConfirmationValidator` validation when `_confirmation`'s value is `false`.
- `#include?`, `#has_key?`, `#key?`, `#delete` and `#full_messages_for`.
+ *bogdanvlviv*
- *Kenichi Kamiya*
+* Allow passing a Proc or Symbol to length validator options.
-* Removed deprecated `:tokenizer` in the length validator.
+ *Matt Rohrer*
- *Rafael Mendonça França*
+* Add method `#merge!` for `ActiveModel::Errors`.
-* Removed deprecated methods in `ActiveModel::Errors`.
+ *Jahfer Husain*
- `#get`, `#set`, `[]=`, `add_on_empty` and `add_on_blank`.
+* Fix regression in numericality validator when comparing Decimal and Float input
+ values with more scale than the schema.
- *Rafael Mendonça França*
+ *Bradley Priest*
+* Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
-Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md) for previous changes.
+ Change `#keys` to only return the keys that don't have empty messages.
+
+ Change `#values` to only return the not empty values.
+
+ Example:
+
+ # Before
+ person = Person.new
+ person.errors.keys # => []
+ person.errors.values # => []
+ person.errors.messages # => {}
+ person.errors[:name] # => []
+ person.errors.messages # => {:name => []}
+ person.errors.keys # => [:name]
+ person.errors.values # => [[]]
+
+ # After
+ person = Person.new
+ person.errors.keys # => []
+ person.errors.values # => []
+ person.errors.messages # => {}
+ person.errors[:name] # => []
+ person.errors.messages # => {:name => []}
+ person.errors.keys # => []
+ person.errors.values # => []
+
+ *bogdanvlviv*
+
+
+Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index ac810e86d0..1cb3add0fc 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2017 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
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index 77f5761993..1aaf4813ea 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -246,16 +246,16 @@ Source code can be downloaded as part of the Rails project on GitHub
Active Model is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index c7f97a4258..d39f50a962 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -1,6 +1,6 @@
-require "rake/testtask"
+# frozen_string_literal: true
-dir = File.dirname(__FILE__)
+require "rake/testtask"
task default: :test
@@ -8,7 +8,7 @@ task :package
Rake::TestTask.new do |t|
t.libs << "test"
- t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb")
+ t.test_files = Dir.glob("#{__dir__}/test/cases/**/*_test.rb")
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
@@ -16,8 +16,8 @@ end
namespace :test do
task :isolated do
- Dir.glob("#{dir}/test/**/*_test.rb").all? do |file|
- sh(Gem.ruby, "-w", "-I#{dir}/lib", "-I#{dir}/test", file)
+ Dir.glob("#{__dir__}/test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file)
end || raise("Failures")
end
end
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index fd715f6ba9..a070a2898c 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -1,4 +1,6 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+# frozen_string_literal: true
+
+version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -18,5 +20,10 @@ Gem::Specification.new do |s|
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"]
s.require_path = "lib"
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activemodel",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md"
+ }
+
s.add_dependency "activesupport", version
end
diff --git a/activemodel/bin/test b/activemodel/bin/test
index a7beb14b27..c53377cc97 100755
--- a/activemodel/bin/test
+++ b/activemodel/bin/test
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
COMPONENT_ROOT = File.expand_path("..", __dir__)
-require File.expand_path("../tools/test", COMPONENT_ROOT)
+require_relative "../../tools/test"
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 2389c858d5..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-2017 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
@@ -28,6 +30,8 @@ require "active_model/version"
module ActiveModel
extend ActiveSupport::Autoload
+ autoload :Attribute
+ autoload :Attributes
autoload :AttributeAssignment
autoload :AttributeMethods
autoload :BlockValidator, "active_model/validator"
@@ -43,6 +47,7 @@ module ActiveModel
autoload :SecurePassword
autoload :Serialization
autoload :Translation
+ autoload :Type
autoload :Validations
autoload :Validator
@@ -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..27f3e38e31
--- /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
+
+ attr_reader :original_attribute
+ alias_method :assigned?, :original_attribute
+
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ private
+ 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
+ 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..f274b687d4
--- /dev/null
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -0,0 +1,30 @@
+# 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
+
+ protected
+
+ 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 7dad3b6dff..aa931119ff 100644
--- a/activemodel/lib/active_model/attribute_assignment.rb
+++ b/activemodel/lib/active_model/attribute_assignment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/hash/keys"
module ActiveModel
@@ -19,15 +21,15 @@ 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."
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))
@@ -42,8 +44,9 @@ module ActiveModel
end
def _assign_attribute(k, v)
- if respond_to?("#{k}=")
- public_send("#{k}=", v)
+ setter = :"#{k}="
+ if respond_to?(setter)
+ public_send(setter, v)
else
raise UnknownAttributeError.new(self, k)
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 09825cf861..888a431e5f 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "concurrent/map"
-require "mutex_m"
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,13 +328,11 @@ 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
+
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
@@ -472,5 +470,9 @@ module ActiveModel
def missing_attribute(attr_name, stack)
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
+
+ def _read_attribute(attr)
+ __send__(attr)
+ 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..c67e1b809a
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -0,0 +1,116 @@
+# 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_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[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
+
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
+ # Workaround for Ruby 2.2 "private attribute?" warning.
+ protected
+
+ attr_reader :attributes, :forced_changes
+
+ private
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ 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..54a5dd4064
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+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..758eb830fc
--- /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)
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @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
+ materialize
+ end
+
+ def marshal_load(delegate_hash)
+ @delegate_hash = delegate_hash
+ @types = {}
+ @values = {}
+ @additional_types = {}
+ @materialized = true
+ end
+
+ protected
+
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
+
+ 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
+
+ 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..4ea945b956
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -0,0 +1,41 @@
+# 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
+
+ protected
+
+ 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..cac461b549
--- /dev/null
+++ b/activemodel/lib/active_model/attributes.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+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_methods(name)
+ end
+
+ private
+
+ def define_method_attribute=(name)
+ safe_name = name.unpack("h*".freeze).first
+ 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
+ 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
+
+ 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
+
+ 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.dup.freeze
+ end
+ end
+ }
+ end
+end
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index eac2761433..8fa9680cb1 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/array/extract_options"
module ActiveModel
@@ -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,8 +103,8 @@ 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 = {
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 12687c70d3..cdc1282817 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
#
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 6e0af99ad7..d2ebd18107 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,5 +1,8 @@
+# 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
@@ -128,6 +131,24 @@ module ActiveModel
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
+
+ def changes_applied # :nodoc:
+ @previously_changed = changes
+ @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 have unsaved changes, +false+ otherwise.
#
# person.changed? # => false
@@ -146,6 +167,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>.
#
@@ -153,7 +228,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.
@@ -164,45 +241,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, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
- !!changes_include?(attr) &&
- (to == OPTION_NOT_GIVEN || to == __send__(attr)) &&
- (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
- end
+ 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) #: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?
@@ -212,21 +295,9 @@ 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+.
@@ -236,15 +307,16 @@ module ActiveModel
# 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+.
@@ -255,18 +327,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 9df4ca51fe..275e3f1313 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,3 +1,5 @@
+# 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"
@@ -93,6 +95,18 @@ module ActiveModel
@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"]
@@ -132,15 +146,6 @@ module ActiveModel
#
# person.errors[:name] # => ["cannot be nil"]
# person.errors['name'] # => ["cannot be nil"]
- #
- # Note that, if you try to get errors of an attribute which has
- # no errors associated with it, this method will instantiate
- # an empty error list for it and +keys+ will return an array
- # of error keys which includes this attribute.
- #
- # person.errors.keys # => []
- # person.errors[:name] # => []
- # person.errors.keys # => [:name]
def [](attribute)
messages[attribute.to_sym]
end
@@ -181,7 +186,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.
@@ -189,7 +196,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.
@@ -313,9 +322,13 @@ module ActiveModel
# 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
+ self.details[attribute].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
# Returns all the full error messages in an array.
@@ -389,21 +402,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)
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index 45ab8a2ca1..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.
#
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 4a8ee915cf..3c344fe854 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
@@ -6,9 +8,9 @@ module ActiveModel
module VERSION
MAJOR = 5
- MINOR = 1
+ MINOR = 2
TINY = 0
- PRE = "alpha"
+ PRE = "beta2"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 291a545528..34d9ac6c96 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
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index 945a5402a3..fc52cd4fdf 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# == Active \Model \Basic \Model
#
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 9853cf38fe..dfccd03cd8 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
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/redefine_method"
module ActiveModel
class Name
@@ -47,7 +49,7 @@ module ActiveModel
# :method: <=>
#
# :call-seq:
- # ==(other)
+ # <=>(other)
#
# Equivalent to <tt>String#<=></tt>.
#
@@ -216,7 +218,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
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index 1671eb7bd4..a9cdabba00 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"
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 1c0fe92bc0..86f051f5ce 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
@@ -18,7 +20,7 @@ module ActiveModel
#
# The following validations are added automatically:
# * Password must be present on creation
- # * Password length should be less than or equal to 72 characters
+ # * Password length should be less than or equal to 72 bytes
# * Confirmation of password (using a +password_confirmation+ attribute)
#
# If password confirmation validation is not needed, simply leave out the
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 77834f26fc..47cb81bee5 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index a9d92eb92a..25e1541d66 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/json"
module ActiveModel
@@ -10,8 +12,7 @@ module ActiveModel
included do
extend ActiveModel::Naming
- class_attribute :include_root_in_json, instance_writer: false
- 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
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index 35fc7cf743..f3d0d3dc27 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# == Active \Model \Translation
#
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index b8e6d2376b..1d7a26fff5 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/type/helpers"
require "active_model/type/value"
@@ -21,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 ActiveRecord::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
@@ -38,6 +32,10 @@ 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)
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index 3b629682fe..89e43bcc5f 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/type/integer"
module ActiveModel
diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
index 819e4e4a96..dc2eca18be 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:
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f2a47370a3..bcdbab0343 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
# == Active \Model \Type \Boolean
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 6e313fbca8..8cecc16d0f 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:
@@ -12,7 +14,7 @@ module ActiveModel
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
end
private
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index 5cb0077e45..9641bf45ee 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,6 +12,10 @@ module ActiveModel
:datetime
end
+ def serialize(value)
+ super(cast(value))
+ end
+
private
def cast_value(value)
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index 541a12c8a1..e8ee18c00e 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "bigdecimal/util"
module ActiveModel
@@ -21,8 +23,14 @@ module ActiveModel
case value
when ::Float
convert_float_to_big_decimal(value)
- when ::Numeric, ::String
+ 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
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 4d0d2771a0..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:
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index 82cd9ebe98..403f0a9e6b 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/type/helpers/accepts_multiparameter_time"
require "active_model/type/helpers/numeric"
require "active_model/type/helpers/mutable"
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 f783d286c5..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,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
module Helpers # :nodoc: all
@@ -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 f3a17a1698..1cbea644c4 100644
--- a/activemodel/lib/active_model/type/helpers/mutable.rb
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
module Helpers # :nodoc: all
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index 275822b738..16e14f9e5f 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
module Helpers # :nodoc: all
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index e57a52104b..250c4021c6 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/time/zones"
module ActiveModel
@@ -38,7 +40,7 @@ module ActiveModel
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ value.to_s(:db).inspect
end
def user_input_in_time_zone(value)
diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb
index 58268540e5..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:
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index 230e45ba60..fe396998a3 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(*)
@@ -48,7 +50,7 @@ module ActiveModel
def ensure_in_range(value)
unless range.cover?(value)
- raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
end
end
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
index 2d5dd366eb..7272d7b0c5 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
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index c7e0208a5a..36f13945b1 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
@@ -12,7 +14,12 @@ module ActiveModel
private
def cast_value(value)
- ::String.new(super)
+ case value
+ when ::String then ::String.new(value)
+ when true then "t".freeze
+ when false then "f".freeze
+ else value.to_s
+ end
end
end
end
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index 54d6214e81..ad7ba0351a 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:
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 7e9ae92245..a8ea6a2c22 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,6 +86,10 @@ module ActiveModel
false
end
+ def value_constructed_by_mass_assignment?(_value) # :nodoc:
+ false
+ end
+
def map(value) # :nodoc:
yield value
end
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index d460068830..7f14d102dd 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,3 +1,5 @@
+# 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"
@@ -49,8 +51,7 @@ module ActiveModel
private :validation_context=
define_callbacks :validate, scope: :name
- class_attribute :_validators, instance_writer: false
- self._validators = Hash.new { |h, k| h[k] = [] }
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
end
module ClassMethods
@@ -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
@@ -432,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 e11005b9ba..f35e4dec7f 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
class AcceptanceValidator < EachValidator # :nodoc:
@@ -95,7 +97,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 4e94422cf1..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
@@ -52,15 +54,18 @@ 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.
@@ -91,15 +96,18 @@ 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
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index 18f1056e2b..0b9b5ce6a1 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/range"
module ActiveModel
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 33ca6f6946..1b5d5b09ab 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
class ConfirmationValidator < EachValidator # :nodoc:
@@ -7,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))
@@ -69,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 82a1893823..3be7ab6ba8 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
@@ -38,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 fa183885ab..7c3f091473 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module ActiveModel
module Validations
@@ -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 0ea6012a5d..3104e7e329 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/validations/clusivity"
module ActiveModel
@@ -36,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 de1524829e..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
@@ -31,8 +31,8 @@ module ActiveModel
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
@@ -45,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
@@ -112,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 2297faaee5..31750ba78e 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
@@ -36,7 +38,9 @@ module ActiveModel
return
end
- unless raw_value.is_a?(Numeric)
+ if raw_value.is_a?(Numeric)
+ value = raw_value
+ else
value = parse_raw_value_as_a_number(raw_value)
end
@@ -63,27 +67,28 @@ module ActiveModel
private
- def is_number?(raw_value) # :doc:
+ def is_number?(raw_value)
!parse_raw_value_as_a_number(raw_value).nil?
rescue ArgumentError, TypeError
false
end
- def parse_raw_value_as_a_number(raw_value) # :doc:
+ 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) # :doc:
+ def is_integer?(raw_value)
/\A[+-]?\d+\z/ === raw_value.to_s
end
- def filtered_options(value) # :doc:
+ def filtered_options(value)
filtered = options.except(*RESERVED_OPTIONS)
filtered[:value] = value
filtered
end
- def allow_only_integer?(record) # :doc:
+ def allow_only_integer?(record)
case options[:only_integer]
when Symbol
record.send(options[:only_integer])
@@ -103,7 +108,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
@@ -134,7 +139,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 0c11cf4265..8787a75afa 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module ActiveModel
module Validations
@@ -29,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 0ce5935f3a..e28e7e9219 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/hash/slice"
module ActiveModel
@@ -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
#
@@ -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+
@@ -153,7 +154,7 @@ module ActiveModel
# 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
- [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
end
def _parse_validates_options(options)
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index e3f7a9bcb2..d777ac836e 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/array/extract_options"
module ActiveModel
@@ -61,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 98234e9b6b..e17c3ca7b3 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/core_ext/module/anonymous"
module ActiveModel
@@ -82,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
@@ -97,7 +99,7 @@ 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.chomp("_validator").to_sym unless anonymous?
end
@@ -109,8 +111,8 @@ module ActiveModel
# 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
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 6e7fd227fd..dd817f5639 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require_relative "gem_version"
module ActiveModel
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index fa41d1b95f..5ecf0a69c4 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/hash_with_indifferent_access"
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index 4767accb7c..d2837ec894 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class ModelWithAttributes
@@ -116,7 +118,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
test "#define_attribute_method does not generate attribute method if already defined in attribute module" do
klass = Class.new(ModelWithAttributes)
- klass.generated_attribute_methods.module_eval do
+ klass.send(:generated_attribute_methods).module_eval do
def foo
"<3"
end
diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..6e522d6c80
--- /dev/null
+++ b/activemodel/test/cases/attribute_set_test.rb
@@ -0,0 +1,257 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_model/attribute_set"
+
+module ActiveModel
+ class AttributeSetTest < ActiveModel::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: "3.3")
+
+ assert_equal "3.3", attributes[:foo].value_before_type_cast
+ assert_nil attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash, but does not dup the attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: "foo")
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << "bar"
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal "foobar", attributes[:bar].value
+ assert_equal "foobar", duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: "foo")
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << "bar"
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal "foo", attributes[:bar].value
+ assert_equal "foobar", duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "to_hash maintains order" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
+
+ attributes[:bar]
+ hash = attributes.to_h
+
+ assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.key?(:foo)
+ assert_not attributes.key?(:bar)
+ end
+
+ test "unknown attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.key?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value returns nil for unknown attributes when types has a default" do
+ types = Hash.new(Type::Value.new)
+ builder = AttributeSet::Builder.new(types)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
+ assert_equal "bar!", value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ test "the primary_key is always initialized" do
+ defaults = { foo: Attribute.from_user(:foo, nil, nil) }
+ builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
+ attributes = builder.build_from_database
+
+ assert attributes.key?(:foo)
+ assert_equal [:foo], attributes.keys
+ assert attributes[:foo].initialized?
+ end
+
+ class MyType
+ def cast(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def deserialize(value)
+ return if value.nil?
+ value + " from database"
+ end
+
+ def assert_valid_value(*)
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: "1.1")
+ end
+
+ test "freezing doesn't prevent the set from materializing" do
+ builder = AttributeSet::Builder.new(foo: Type::String.new)
+ attributes = builder.build_from_database(foo: "1")
+
+ attributes.freeze
+ assert_equal({ foo: "1" }, attributes.to_hash)
+ end
+
+ test "#accessed_attributes returns only attributes which have been read" do
+ builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+
+ assert_equal [], attributes.accessed
+
+ attributes.fetch_value(:foo)
+
+ assert_equal [:foo], attributes.accessed
+ end
+
+ test "#map returns a new attribute set with the changes applied" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ new_attributes = attributes.map do |attr|
+ attr.with_cast_value(attr.value + 1)
+ end
+
+ assert_equal 2, new_attributes.fetch_value(:foo)
+ assert_equal 3, new_attributes.fetch_value(:bar)
+ end
+
+ test "comparison for equality is correctly implemented" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ attributes2 = builder.build_from_database(foo: "1", bar: "2")
+ attributes3 = builder.build_from_database(foo: "2", bar: "2")
+
+ assert_equal attributes, attributes2
+ assert_not_equal attributes2, attributes3
+ end
+ end
+end
diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..14d86cef97
--- /dev/null
+++ b/activemodel/test/cases/attribute_test.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ class AttributeTest < ActiveModel::TestCase
+ setup do
+ @type = Minitest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:deserialize, "type cast from database", ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal "type cast from database", type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:cast, "type cast from user", ["a value"])
+ attribute = Attribute.from_user(nil, "a value", @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal "type cast from user", type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:deserialize, "from the database", ["whatever"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal "from the database", type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:deserialize, false, ["whatever"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, "raw value", @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal "raw value", raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:deserialize, "read from database", ["whatever"])
+ @type.expect(:serialize, "ready for database", ["read from database"])
+ attribute = Attribute.from_database(nil, "whatever", @type)
+
+ serialize = attribute.value_for_database
+
+ assert_equal "ready for database", serialize
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:cast, "read from user", ["whatever"])
+ @type.expect(:serialize, "ready for database", ["read from user"])
+ attribute = Attribute.from_user(nil, "whatever", @type)
+
+ serialize = attribute.value_for_database
+
+ assert_equal "ready for database", serialize
+ end
+
+ test "duping dups the value" do
+ @type.expect(:deserialize, "type cast".dup, ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << " foo"
+
+ assert_equal "type cast foo", value_from_orig
+ assert_equal "type cast", value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:deserialize, false, ["a value"])
+ attribute = Attribute.from_database(nil, "a value", @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, "a value", @type)
+ attribute.dup
+ end
+
+ class MyType
+ def cast(value)
+ value + " from user"
+ end
+
+ def deserialize(value)
+ value + " from database"
+ end
+
+ def assert_valid_value(*)
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+
+ test "attributes equal other attributes with the same constructor arguments" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Integer.new)
+ assert_equal first, second
+ end
+
+ test "attributes do not equal attributes with different names" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:bar, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different types" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Float.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different values" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 2, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes of other classes" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_user(:foo, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "an attribute has not been read by default" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ assert_not attribute.has_been_read?
+ end
+
+ test "an attribute has been read when its value is calculated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ attribute.value
+ assert attribute.has_been_read?
+ end
+
+ test "an attribute is not changed if it hasn't been assigned or mutated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+
+ refute attribute.changed?
+ end
+
+ test "an attribute is changed if it's been assigned a new value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ changed = attribute.with_value_from_user(2)
+
+ assert changed.changed?
+ end
+
+ test "an attribute is not changed if it's assigned the same value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ unchanged = attribute.with_value_from_user(1)
+
+ refute unchanged.changed?
+ end
+
+ test "an attribute can not 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)
+
+ assert_not attribute.changed_in_place?
+ end
+
+ test "an attribute is changed if it has been mutated" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ attribute.value << "!"
+
+ assert attribute.changed_in_place?
+ assert attribute.changed?
+ end
+
+ test "an attribute can forget its changes" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ changed = attribute.with_value_from_user("foo")
+ forgotten = changed.forgetting_assignment
+
+ assert changed.changed? # sanity check
+ refute forgotten.changed?
+ end
+
+ test "with_value_from_user validates the value" do
+ type = Type::Value.new
+ type.define_singleton_method(:assert_valid_value) do |value|
+ if value == 1
+ raise ArgumentError
+ end
+ end
+
+ attribute = Attribute.from_database(:foo, 1, type)
+ assert_equal 1, attribute.value
+ assert_equal 2, attribute.with_value_from_user(2).value
+ assert_raises ArgumentError do
+ attribute.with_value_from_user(1)
+ end
+ end
+
+ test "with_type preserves mutations" do
+ attribute = Attribute.from_database(:foo, "".dup, Type::Value.new)
+ attribute.value << "1"
+
+ assert_equal 1, attribute.with_type(Type::Integer.new).value
+ end
+ end
+end
diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb
new file mode 100644
index 0000000000..83a86371e0
--- /dev/null
+++ b/activemodel/test/cases/attributes_dirty_test.rb
@@ -0,0 +1,205 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class AttributesDirtyTest < ActiveModel::TestCase
+ class DirtyModel
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include ActiveModel::Dirty
+ attribute :name, :string
+ attribute :color, :string
+ attribute :size, :integer
+
+ def save
+ changes_applied
+ end
+
+ def reload
+ clear_changes_information
+ end
+ end
+
+ setup do
+ @model = DirtyModel.new
+ end
+
+ test "setting attribute will result in change" do
+ assert !@model.changed?
+ assert !@model.name_changed?
+ @model.name = "Ringo"
+ assert @model.changed?
+ assert @model.name_changed?
+ end
+
+ test "list of changed attribute keys" do
+ assert_equal [], @model.changed
+ @model.name = "Paul"
+ assert_equal ["name"], @model.changed
+ end
+
+ test "changes to attribute values" do
+ assert !@model.changes["name"]
+ @model.name = "John"
+ assert_equal [nil, "John"], @model.changes["name"]
+ end
+
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
+ test "changes accessible through both strings and symbols" do
+ @model.name = "David"
+ assert_not_nil @model.changes[:name]
+ assert_not_nil @model.changes["name"]
+ end
+
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = "Rafael"
+ assert @model.attribute_changed?(:name)
+ end
+
+ test "attribute mutation" do
+ @model.name = "Yam"
+ @model.save
+ assert !@model.name_changed?
+ @model.name.replace("Hadad")
+ assert @model.name_changed?
+ end
+
+ test "resetting attribute" do
+ @model.name = "Bob"
+ @model.restore_name!
+ assert_nil @model.name
+ assert !@model.name_changed?
+ end
+
+ test "setting color to same value should not result in change being recorded" do
+ @model.color = "red"
+ assert @model.color_changed?
+ @model.save
+ assert !@model.color_changed?
+ assert !@model.changed?
+ @model.color = "red"
+ assert !@model.color_changed?
+ assert !@model.changed?
+ end
+
+ test "saving should reset model's changed status" do
+ @model.name = "Alf"
+ assert @model.changed?
+ @model.save
+ assert !@model.changed?
+ assert !@model.name_changed?
+ end
+
+ test "saving should preserve previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"]
+ end
+
+ test "setting new attributes should not affect previous changes" do
+ @model.name = "Jericho Cane"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ assert_equal [nil, "Jericho Cane"], @model.name_previous_change
+ end
+
+ test "saving should preserve model's previous changed status" do
+ @model.name = "Jericho Cane"
+ @model.save
+ assert @model.name_previously_changed?
+ end
+
+ test "previous value is preserved when changed after save" do
+ assert_equal({}, @model.changed_attributes)
+ @model.name = "Paul"
+ assert_equal({ "name" => nil }, @model.changed_attributes)
+
+ @model.save
+
+ @model.name = "John"
+ assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ end
+
+ test "changing the same attribute multiple times retains the correct original value" do
+ @model.name = "Otto"
+ @model.save
+ @model.name = "DudeFella ManGuy"
+ @model.name = "Mr. Manfredgensonton"
+ assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
+ assert_equal @model.name_was, "Otto"
+ end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert @model.size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = "Dmitry"
+ @model.name_changed?
+ @model.save
+ @model.name = "Bob"
+
+ assert_equal [nil, "Dmitry"], @model.previous_changes["name"]
+ assert_equal "Dmitry", @model.changed_attributes["name"]
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes
+
+ assert_not @model.changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "Red", @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = "Dmitry"
+ @model.color = "Red"
+ @model.save
+ @model.name = "Bob"
+ @model.color = "White"
+
+ @model.restore_attributes(["name"])
+
+ assert @model.changed?
+ assert_equal "Dmitry", @model.name
+ assert_equal "White", @model.color
+ end
+
+ test "changing the attribute reports a change only when the cast value changes" do
+ @model.size = "2.3"
+ @model.save
+ @model.size = "2.1"
+
+ assert_equal false, @model.changed?
+
+ @model.size = "5.1"
+
+ assert_equal true, @model.changed?
+ assert_equal true, @model.size_changed?
+ assert_equal({ "size" => [2, 5] }, @model.changes)
+ end
+end
diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..e43bf15335
--- /dev/null
+++ b/activemodel/test/cases/attributes_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveModel
+ class AttributesTest < ActiveModel::TestCase
+ class ModelForAttributesTest
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+
+ attribute :integer_field, :integer
+ attribute :string_field, :string
+ attribute :decimal_field, :decimal
+ attribute :string_with_default, :string, default: "default string"
+ attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) }
+ attribute :boolean_field, :boolean
+ end
+
+ class ChildModelForAttributesTest < ModelForAttributesTest
+ end
+
+ class GrandchildModelForAttributesTest < ChildModelForAttributesTest
+ attribute :integer_field, :string
+ end
+
+ test "properties assignment" do
+ data = ModelForAttributesTest.new(
+ integer_field: "2.3",
+ string_field: "Rails FTW",
+ decimal_field: "12.3",
+ boolean_field: "0"
+ )
+
+ assert_equal 2, data.integer_field
+ assert_equal "Rails FTW", data.string_field
+ assert_equal BigDecimal("12.3"), data.decimal_field
+ assert_equal "default string", data.string_with_default
+ assert_equal Date.new(2016, 1, 1), data.date_field
+ assert_equal false, data.boolean_field
+
+ data.integer_field = 10
+ data.string_with_default = nil
+ data.boolean_field = "1"
+
+ assert_equal 10, data.integer_field
+ assert_nil data.string_with_default
+ assert_equal true, data.boolean_field
+ end
+
+ test "nonexistent attribute" do
+ assert_raise ActiveModel::UnknownAttributeError do
+ ModelForAttributesTest.new(nonexistent: "nonexistent")
+ end
+ end
+
+ test "children inherit attributes" do
+ data = ChildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal 4, data.integer_field
+ end
+
+ test "children can override parents" do
+ data = GrandchildModelForAttributesTest.new(integer_field: "4.4")
+
+ assert_equal "4.4", data.integer_field
+ end
+ end
+end
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index f85cd7dec4..a5d29d0f22 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class CallbacksTest < ActiveModel::TestCase
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
index 4a93347abc..347896ed50 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/contact"
require "models/helicopter"
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index fdd18d7601..dfe041ff50 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class DirtyTest < ActiveModel::TestCase
@@ -96,7 +98,7 @@ class DirtyTest < ActiveModel::TestCase
end
test "attribute mutation" do
- @model.instance_variable_set("@name", "Yam")
+ @model.instance_variable_set("@name", "Yam".dup)
assert !@model.name_changed?
@model.name.replace("Hadad")
assert !@model.name_changed?
@@ -217,4 +219,8 @@ class DirtyTest < ActiveModel::TestCase
assert_equal "Dmitry", @model.name
assert_equal "White", @model.color
end
+
+ test "model can be dup-ed without Attributes" do
+ assert @model.dup
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 0872084cf5..d5c282b620 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/string/strip"
require "yaml"
@@ -99,6 +101,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["omg", "zomg"], errors.values
end
+ test "values returns an empty array after try to get a message only" do
+ errors = ActiveModel::Errors.new(self)
+ errors.messages[:foo]
+ errors.messages[:baz]
+
+ assert_equal [], errors.values
+ end
+
test "keys returns the error keys" do
errors = ActiveModel::Errors.new(self)
errors.messages[:foo] << "omg"
@@ -107,6 +117,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal [:foo, :baz], errors.keys
end
+ test "keys returns an empty array after try to get a message only" do
+ errors = ActiveModel::Errors.new(self)
+ errors.messages[:foo]
+ errors.messages[:baz]
+
+ assert_equal [], errors.keys
+ end
+
test "detecting whether there are errors with empty?, blank?, include?" do
person = Person.new
person.errors[:foo]
@@ -205,6 +223,13 @@ class ErrorsTest < ActiveModel::TestCase
assert !person.errors.added?(:name)
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 !person.errors.added?(:name, :used)
+ end
+
test "size calculates the number of error messages" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -359,6 +384,18 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal [:name], person.errors.details.keys
end
+ test "merge errors" do
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :invalid)
+
+ person = Person.new
+ person.errors.add(:name, :blank)
+ person.errors.merge!(errors)
+
+ assert_equal({ name: ["can't be blank", "is invalid"] }, person.errors.messages)
+ assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details)
+ end
+
test "errors are marshalable" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
diff --git a/activemodel/test/cases/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb
index d8cc72e662..0fd0a2f8ee 100644
--- a/activemodel/test/cases/forbidden_attributes_protection_test.rb
+++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/hash/indifferent_access"
require "models/account"
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index eeb5c85a48..91fb9d0a7c 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model"
# Show backtraces for deprecated behavior for quicker cleanup.
diff --git a/activemodel/test/cases/lint_test.rb b/activemodel/test/cases/lint_test.rb
index 7a817d7c01..d62c80b71a 100644
--- a/activemodel/test/cases/lint_test.rb
+++ b/activemodel/test/cases/lint_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class LintTest < ActiveModel::TestCase
diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb
index ba87cd1506..b24d7e3571 100644
--- a/activemodel/test/cases/model_test.rb
+++ b/activemodel/test/cases/model_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class ModelTest < ActiveModel::TestCase
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
index d5cb1a62bc..009f1f47af 100644
--- a/activemodel/test/cases/naming_test.rb
+++ b/activemodel/test/cases/naming_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/contact"
require "models/sheep"
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index a56b26b5ee..ff5022e960 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/testing/isolation"
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 77ce86f392..d19e81a119 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/user"
require "models/visitor"
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index f78efd2f0c..9002982e7f 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/object/instance_variables"
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index d15ba64eb0..aae98c9fe4 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/contact"
require "active_support/core_ext/object/instance_variables"
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index 9972f9daea..cd75afec9e 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/person"
diff --git a/activemodel/test/cases/type/big_integer_test.rb b/activemodel/test/cases/type/big_integer_test.rb
index 56002b7cc6..0fa0200df4 100644
--- a/activemodel/test/cases/type/big_integer_test.rb
+++ b/activemodel/test/cases/type/big_integer_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb
index e9c2ccfca4..3221a73e49 100644
--- a/activemodel/test/cases/type/binary_test.rb
+++ b/activemodel/test/cases/type/binary_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb
index 92e5aebfb7..2d33579595 100644
--- a/activemodel/test/cases/type/boolean_test.rb
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb
index 0cc90e99d3..e8cf178612 100644
--- a/activemodel/test/cases/type/date_test.rb
+++ b/activemodel/test/cases/type/date_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb
index 75a7fc686e..60f62becc2 100644
--- a/activemodel/test/cases/type/date_time_test.rb
+++ b/activemodel/test/cases/type/date_time_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index 46a913258e..c0cf6ce590 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -1,19 +1,28 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
class DecimalTest < ActiveModel::TestCase
def test_type_cast_decimal
type = Decimal.new
- assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.cast(123.0)
- assert_equal BigDecimal.new("1"), type.cast(:"1")
+ assert_equal BigDecimal("0"), type.cast(BigDecimal("0"))
+ assert_equal BigDecimal("123"), type.cast(123.0)
+ assert_equal BigDecimal("1"), type.cast(:"1")
+ end
+
+ def test_type_cast_decimal_from_invalid_string
+ type = Decimal.new
+ assert_nil type.cast("")
+ assert_equal BigDecimal("1"), type.cast("1ignore")
+ assert_equal BigDecimal("0"), type.cast("bad1")
+ assert_equal BigDecimal("0"), type.cast("bad")
end
def test_type_cast_decimal_from_float_with_large_precision
type = Decimal.new(precision: ::Float::DIG + 2)
- assert_equal BigDecimal.new("123.0"), type.cast(123.0)
+ assert_equal BigDecimal("123.0"), type.cast(123.0)
end
def test_type_cast_from_float_with_unspecified_precision
@@ -39,7 +48,7 @@ module ActiveModel
def test_type_cast_decimal_from_object_responding_to_d
value = Object.new
def value.to_d
- BigDecimal.new("1")
+ BigDecimal("1")
end
type = Decimal.new
assert_equal BigDecimal("1"), type.cast(value)
diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb
index 2e34f57f7e..28318e06f8 100644
--- a/activemodel/test/cases/type/float_test.rb
+++ b/activemodel/test/cases/type/float_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
@@ -9,6 +10,14 @@ module ActiveModel
assert_equal 1.0, type.cast("1")
end
+ def test_type_cast_float_from_invalid_string
+ type = Type::Float.new
+ assert_nil type.cast("")
+ assert_equal 1.0, type.cast("1ignore")
+ assert_equal 0.0, type.cast("bad1")
+ assert_equal 0.0, type.cast("bad")
+ end
+
def test_changing_float
type = Type::Float.new
diff --git a/activemodel/test/cases/type/immutable_string_test.rb b/activemodel/test/cases/type/immutable_string_test.rb
index 23e58974fb..751f753ddb 100644
--- a/activemodel/test/cases/type/immutable_string_test.rb
+++ b/activemodel/test/cases/type/immutable_string_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
index 2b9b03f3cf..8c5d18c9b3 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
require "active_support/core_ext/numeric/time"
module ActiveModel
@@ -7,6 +8,7 @@ module ActiveModel
class IntegerTest < ActiveModel::TestCase
test "simple values" do
type = Type::Integer.new
+ assert_nil type.cast("")
assert_equal 1, type.cast(1)
assert_equal 1, type.cast("1")
assert_equal 1, type.cast("1ignore")
diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb
index 927b6d0307..0633ea2538 100644
--- a/activemodel/test/cases/type/registry_test.rb
+++ b/activemodel/test/cases/type/registry_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
index 222083817e..825c8bb246 100644
--- a/activemodel/test/cases/type/string_test.rb
+++ b/activemodel/test/cases/type/string_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
@@ -12,16 +13,25 @@ module ActiveModel
end
test "cast strings are mutable" do
- s = "foo"
type = Type::String.new
+
+ s = "foo".dup
assert_equal false, type.cast(s).frozen?
+ assert_equal false, s.frozen?
+
+ f = "foo".freeze
+ assert_equal false, type.cast(f).frozen?
+ assert_equal true, f.frozen?
end
test "values are duped coming out" do
- s = "foo"
type = Type::String.new
+
+ s = "foo"
assert_not_same s, type.cast(s)
+ assert_equal s, type.cast(s)
assert_not_same s, type.deserialize(s)
+ assert_equal s, type.deserialize(s)
end
end
end
diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb
index 0cc4d33caa..f7102d1e97 100644
--- a/activemodel/test/cases/type/time_test.rb
+++ b/activemodel/test/cases/type/time_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/type/value_test.rb b/activemodel/test/cases/type/value_test.rb
index d8b3e7f164..55b5d9d584 100644
--- a/activemodel/test/cases/type/value_test.rb
+++ b/activemodel/test/cases/type/value_test.rb
@@ -1,5 +1,6 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
index 833f694c5a..801577474a 100644
--- a/activemodel/test/cases/validations/absence_validation_test.rb
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
require "models/person"
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index fbd994e914..c5f54b1868 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
index f2e4a5946d..ff3cf61746 100644
--- a/activemodel/test/cases/validations/callbacks_test.rb
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Dog
@@ -57,6 +59,18 @@ class DogValidatorWithOnCondition < Dog
def set_after_validation_marker; history << "after_validation_marker" ; end
end
+class DogValidatorWithOnMultipleCondition < Dog
+ before_validation :set_before_validation_marker_on_context_a, on: :context_a
+ before_validation :set_before_validation_marker_on_context_b, on: :context_b
+ after_validation :set_after_validation_marker_on_context_a, on: :context_a
+ after_validation :set_after_validation_marker_on_context_b, on: :context_b
+
+ def set_before_validation_marker_on_context_a; history << "before_validation_marker on context_a"; end
+ def set_before_validation_marker_on_context_b; history << "before_validation_marker on context_b"; end
+ def set_after_validation_marker_on_context_a; history << "after_validation_marker on context_a" ; end
+ def set_after_validation_marker_on_context_b; history << "after_validation_marker on context_b" ; end
+end
+
class DogValidatorWithIfCondition < Dog
before_validation :set_before_validation_marker1, if: -> { true }
before_validation :set_before_validation_marker2, if: -> { false }
@@ -96,6 +110,37 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
assert_equal [], d.history
end
+ def test_on_multiple_condition_is_respected_for_validation_with_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_a)
+ assert_equal ["before_validation_marker on context_a", "after_validation_marker on context_a"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:context_b)
+ assert_equal ["before_validation_marker on context_b", "after_validation_marker on context_b"], d.history
+
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?([:context_a, :context_b])
+ assert_equal([
+ "before_validation_marker on context_a",
+ "before_validation_marker on context_b",
+ "after_validation_marker on context_a",
+ "after_validation_marker on context_b"
+ ], d.history)
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_matching_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?(:save)
+ assert_equal [], d.history
+ end
+
+ def test_on_multiple_condition_is_respected_for_validation_without_context
+ d = DogValidatorWithOnMultipleCondition.new
+ d.valid?
+ assert_equal [], d.history
+ end
+
def test_before_validation_and_after_validation_callbacks_should_be_called
d = DogWithMethodCallbacks.new
d.valid?
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 048d27446e..caea8b65ef 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -16,6 +18,22 @@ class ConditionalValidationTest < ActiveModel::TestCase
assert_equal ["hoo 5"], t.errors["title"]
end
+ def test_if_validation_using_array_of_true_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_true])
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
+ def test_unless_validation_using_array_of_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_false, :condition_is_false])
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
+ end
+
def test_unless_validation_using_method_true
# When the method returns true
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
@@ -24,59 +42,31 @@ class ConditionalValidationTest < ActiveModel::TestCase
assert_empty t.errors[:title]
end
- def test_if_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
+ def test_if_validation_using_array_of_true_and_false_methods
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: [:condition_is_true, :condition_is_false])
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert_empty t.errors[:title]
end
- def test_unless_validation_using_method_false
- # When the method returns false
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not)
- t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
- end
-
- def test_if_validation_using_string_true
- # When the evaluated string returns true
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
- end
- t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
- assert t.invalid?
- assert t.errors[:title].any?
- assert_equal ["hoo 5"], t.errors["title"]
- end
-
- def test_unless_validation_using_string_true
- # When the evaluated string returns true
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
- end
+ def test_unless_validation_using_array_of_true_and_felse_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 t.valid?
assert_empty t.errors[:title]
end
- def test_if_validation_using_string_false
- # When the evaluated string returns false
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
- end
+ def test_if_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
assert_empty t.errors[:title]
end
- def test_unless_validation_using_string_false
- # When the evaluated string returns false
- ActiveSupport::Deprecation.silence do
- Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
- end
+ def test_unless_validation_using_method_false
+ # When the method returns false
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_false)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -121,27 +111,18 @@ class ConditionalValidationTest < ActiveModel::TestCase
assert_equal ["hoo 5"], t.errors["title"]
end
- # previous implementation of validates_presence_of eval'd the
- # string with the wrong binding, this regression test is to
- # ensure that it works correctly
- def test_validation_with_if_as_string
- Topic.validates_presence_of(:title)
- ActiveSupport::Deprecation.silence do
- Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
- end
-
- t = Topic.new
- assert t.invalid?, "A topic without a title should not be valid"
- assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
-
- t.title = "Just a title"
- assert t.valid?, "A topic with a basic title should be valid"
-
- t.title = "A very important title"
- assert t.invalid?, "A topic with an important title, but without an author, should not be valid"
- assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author"
+ def test_validation_using_conbining_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 t.valid?
+ assert_empty t.errors[:title]
+ end
- t.author_name = "Hubert J. Farnsworth"
- assert t.valid?, "A topic with an important title and author should be valid"
+ def test_validation_using_conbining_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 t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["hoo 5"], t.errors["title"]
end
end
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index 7ddf3ad273..8b2c65289b 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -35,6 +37,19 @@ class ConfirmationValidationTest < ActiveModel::TestCase
assert t.valid?
end
+ def test_validates_confirmation_of_with_boolean_attribute
+ Topic.validates_confirmation_of(:approved)
+
+ t = Topic.new(approved: true, approved_confirmation: nil)
+ assert t.valid?
+
+ t.approved_confirmation = false
+ assert t.invalid?
+
+ t.approved_confirmation = true
+ assert t.valid?
+ end
+
def test_validates_confirmation_of_for_ruby_class
Person.validates_confirmation_of :karma
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 06ae4fbecd..68d611e904 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/numeric/time"
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index d7e6bf3707..3ddda2154a 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -107,7 +109,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_with_lambda
- Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\z/ : /\A\S+\z/ }
t = Topic.new
t.title = "digit"
@@ -119,7 +121,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_without_lambda
- Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\z/ : /\A\S+\z/ }
t = Topic.new
t.title = "characters"
@@ -131,7 +133,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_for_ruby_class
- Person.validates_format_of :karma, with: /\A\d+\Z/
+ Person.validates_format_of :karma, with: /\A\d+\z/
p = Person.new
p.karma = "Pixies"
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index f049ee26e8..d3e44945db 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/person"
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index f28cfc0ef5..9cfe189d0e 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/person"
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 5aa43ea4a9..94df0649a9 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/all"
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index 95ee87b401..42f76f3e3c 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -408,4 +410,35 @@ class LengthValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "david").valid?
assert Topic.new("title" => "david2").invalid?
end
+
+ def test_validates_length_of_using_proc_as_maximum
+ Topic.validates_length_of :title, maximum: ->(model) { 5 }
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_proc_as_maximum_with_model_method
+ Topic.send(:define_method, :max_title_length, lambda { 5 })
+ Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length)
+
+ t = Topic.new("title" => "valid", "content" => "whatever")
+ assert t.valid?
+
+ t.title = "notvalid"
+ assert t.invalid?
+ assert t.errors[:title].any?
+ assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title]
+
+ t.title = ""
+ assert t.valid?
+ end
end
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index a1be2de578..5413255e6b 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -18,7 +20,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090)
FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS
INTEGERS = [0, 10, -10] + INTEGER_STRINGS
- BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) }
+ BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal(bd) }
JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
INFINITY = [1.0 / 0.0]
@@ -57,7 +59,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_with_integer_only_and_symbol_as_value
- Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_false
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
@@ -79,10 +81,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_greater_than_using_differing_numeric_types
- Topic.validates_numericality_of :approved, greater_than: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18")
- invalid!([-97.18, BigDecimal.new("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
- valid!([97.19, 98, BigDecimal.new("98"), BigDecimal.new("97.19")])
+ invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
+ valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_using_string_value
@@ -100,10 +102,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types
- Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18")
- invalid!([-97.18, 97.17, 97, BigDecimal.new("97.17"), BigDecimal.new("-97.18")], "must be greater than or equal to 97.18")
- valid!([97.18, 98, BigDecimal.new("97.19")])
+ invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18")
+ valid!([97.18, 98, BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_or_equal_using_string_value
@@ -121,10 +123,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_equal_to_using_differing_numeric_types
- Topic.validates_numericality_of :approved, equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18")
invalid!([-97.18], "must be equal to 97.18")
- valid!([BigDecimal.new("97.18")])
+ valid!([BigDecimal("97.18")])
end
def test_validates_numericality_with_equal_to_using_string_value
@@ -142,10 +144,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_less_than_using_differing_numeric_types
- Topic.validates_numericality_of :approved, less_than: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18")
- invalid!([97.18, BigDecimal.new("97.18")], "must be less than 97.18")
- valid!([-97.0, 97.0, -97, 97, BigDecimal.new("-97"), BigDecimal.new("97")])
+ invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18")
+ valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
end
def test_validates_numericality_with_less_than_using_string_value
@@ -163,10 +165,10 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types
- Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal.new("97.18")
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18")
invalid!([97.19, 98], "must be less than or equal to 97.18")
- valid!([-97.18, BigDecimal.new("-97.18"), BigDecimal.new("97.18")])
+ valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")])
end
def test_validates_numericality_with_less_than_or_equal_using_string_value
@@ -260,6 +262,15 @@ class NumericalityValidationTest < ActiveModel::TestCase
Person.clear_validators!
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
+ topic = Topic.new
+ topic.approved = (base + 1).to_s
+
+ assert topic.invalid?
+ 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" }
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index 642dd0f144..22c2f0af87 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
index 011033606e..7f32f5dc74 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/person"
require "models/topic"
@@ -60,17 +62,23 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_if_as_local_conditions
- Person.validates :karma, presence: true, email: { unless: :condition_is_true }
+ Person.validates :karma, presence: true, email: { if: :condition_is_false }
person = Person.new
person.valid?
assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_if_as_shared_conditions
- Person.validates :karma, presence: true, email: true, if: :condition_is_true
+ Person.validates :karma, presence: true, email: true, if: :condition_is_false
+ person = Person.new
+ assert person.valid?
+ end
+
+ def test_validates_with_unless_as_local_conditions
+ Person.validates :karma, presence: true, email: { unless: :condition_is_true }
person = Person.new
person.valid?
- assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort
+ assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_unless_shared_conditions
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 25c37a572f..024eb1882f 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 5ce86738cd..13ef5e6a31 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
@@ -68,51 +70,15 @@ class ValidatesWithTest < ActiveModel::TestCase
assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE
end
- test "with if statements that return false" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
- end
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with if statements that return true" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
- end
- topic = Topic.new
- assert topic.invalid?
- assert_includes topic.errors[:base], ERROR_MESSAGE
- end
-
- test "with unless statements that return true" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
- end
- topic = Topic.new
- assert topic.valid?
- end
-
- test "with unless statements that returns false" do
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
- end
- topic = Topic.new
- assert topic.invalid?
- assert_includes topic.errors[:base], ERROR_MESSAGE
- end
-
test "passes all configuration options to the validator class" do
topic = Topic.new
validator = Minitest::Mock.new
- validator.expect(:new, validator, [{ foo: :bar, if: "1 == 1", class: Topic }])
+ validator.expect(:new, validator, [{ foo: :bar, if: :condition_is_true, class: Topic }])
validator.expect(:validate, nil, [topic])
validator.expect(:is_a?, false, [Symbol])
validator.expect(:is_a?, false, [String])
- ActiveSupport::Deprecation.silence do
- Topic.validates_with(validator, if: "1 == 1", foo: :bar)
- end
+ Topic.validates_with(validator, if: :condition_is_true, foo: :bar)
assert topic.valid?
validator.verify
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 6647191205..ab8c41bbd0 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/topic"
diff --git a/activemodel/test/models/account.rb b/activemodel/test/models/account.rb
index eed668d38f..40408e5708 100644
--- a/activemodel/test/models/account.rb
+++ b/activemodel/test/models/account.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Account
include ActiveModel::ForbiddenAttributesProtection
diff --git a/activemodel/test/models/blog_post.rb b/activemodel/test/models/blog_post.rb
index 46eba857df..d4b02eeaa7 100644
--- a/activemodel/test/models/blog_post.rb
+++ b/activemodel/test/models/blog_post.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Blog
def self.use_relative_model_naming?
true
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
index 113ab0bc1f..c40a6d6f0e 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Contact
extend ActiveModel::Naming
include ActiveModel::Conversion
diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb
index dc26bb10ff..df605e93d9 100644
--- a/activemodel/test/models/custom_reader.rb
+++ b/activemodel/test/models/custom_reader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CustomReader
include ActiveModel::Validations
diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb
index 933f3c463a..fe82c463d3 100644
--- a/activemodel/test/models/helicopter.rb
+++ b/activemodel/test/models/helicopter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Helicopter
include ActiveModel::Conversion
end
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
index e896e90f98..8dd8ceadad 100644
--- a/activemodel/test/models/person.rb
+++ b/activemodel/test/models/person.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Person
include ActiveModel::Validations
extend ActiveModel::Translation
@@ -7,6 +9,10 @@ class Person
def condition_is_true
true
end
+
+ def condition_is_false
+ false
+ end
end
class Person::Gender
diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb
index 505ed880c1..44e78cbc29 100644
--- a/activemodel/test/models/person_with_validator.rb
+++ b/activemodel/test/models/person_with_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class PersonWithValidator
include ActiveModel::Validations
diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb
index 3fe11043d2..6bb18f95fe 100644
--- a/activemodel/test/models/reply.rb
+++ b/activemodel/test/models/reply.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "models/topic"
class Reply < Topic
diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb
index 7aba055c4f..30dd9ce192 100644
--- a/activemodel/test/models/sheep.rb
+++ b/activemodel/test/models/sheep.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Sheep
extend ActiveModel::Naming
end
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index 192786c096..b0af00ee45 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Topic
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
@@ -21,7 +23,7 @@ class Topic
true
end
- def condition_is_true_but_its_not
+ def condition_is_false
false
end
diff --git a/activemodel/test/models/track_back.rb b/activemodel/test/models/track_back.rb
index 357ee37d6d..728a022db5 100644
--- a/activemodel/test/models/track_back.rb
+++ b/activemodel/test/models/track_back.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Post
class TrackBack
def to_model
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index 6556b1a7d9..e98fd8a0a1 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class User
extend ActiveModel::Callbacks
include ActiveModel::SecurePassword
diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb
index 22ad1a3c3d..9da004ffcc 100644
--- a/activemodel/test/models/visitor.rb
+++ b/activemodel/test/models/visitor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Visitor
extend ActiveModel::Callbacks
include ActiveModel::SecurePassword
diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb
index 9b74d83b37..0c634d8659 100644
--- a/activemodel/test/validators/email_validator.rb
+++ b/activemodel/test/validators/email_validator.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb
index 8639045b0d..e7815d92dc 100644
--- a/activemodel/test/validators/namespace/email_validator.rb
+++ b/activemodel/test/validators/namespace/email_validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "validators/email_validator"
module Namespace