diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/lib/active_model/attribute_methods.rb | 29 | ||||
-rw-r--r-- | activemodel/lib/active_model/attribute_mutation_tracker.rb | 117 | ||||
-rw-r--r-- | activemodel/lib/active_model/attributes.rb | 46 | ||||
-rw-r--r-- | activemodel/lib/active_model/dirty.rb | 133 | ||||
-rw-r--r-- | activemodel/test/cases/attribute_methods_test.rb | 1 | ||||
-rw-r--r-- | activemodel/test/cases/attributes_test.rb | 14 | ||||
-rw-r--r-- | activemodel/test/cases/validations/acceptance_validation_test.rb | 50 | ||||
-rw-r--r-- | activemodel/test/cases/validations/i18n_validation_test.rb | 98 |
8 files changed, 281 insertions, 207 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 5c4670f393..415f1f679b 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -286,12 +286,12 @@ module ActiveModel method_name = matcher.method_name(attr_name) unless instance_method_already_implemented?(method_name) - generate_method = "define_method_#{matcher.method_missing_target}" + generate_method = "define_method_#{matcher.target}" if respond_to?(generate_method, true) 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 + define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s end end end @@ -352,17 +352,18 @@ module ActiveModel def attribute_method_matchers_matching(method_name) attribute_method_matchers_cache.compute_if_absent(method_name) do - # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix - # will match every time. + # Bump plain matcher to last place so that only methods that do not + # match any other pattern match the actual attribute name. + # This is currently only needed to support legacy usage. matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1) - matchers.map { |method| method.match(method_name) }.compact + matchers.map { |matcher| matcher.match(method_name) }.compact end end # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This falls back on `define_method` # and `send` if the given names cannot be compiled. - def define_proxy_call(include_private, mod, name, send, *extra) + def define_proxy_call(include_private, mod, name, target, *extra) defn = if NAME_COMPILABLE_REGEXP.match?(name) "def #{name}(*args)" else @@ -371,34 +372,34 @@ module ActiveModel extra = (extra.map!(&:inspect) << "*args").join(", ") - target = if CALL_COMPILABLE_REGEXP.match?(send) - "#{"self." unless include_private}#{send}(#{extra})" + body = if CALL_COMPILABLE_REGEXP.match?(target) + "#{"self." unless include_private}#{target}(#{extra})" else - "send(:'#{send}', #{extra})" + "send(:'#{target}', #{extra})" end mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 #{defn} - #{target} + #{body} end RUBY end class AttributeMethodMatcher #:nodoc: - attr_reader :prefix, :suffix, :method_missing_target + attr_reader :prefix, :suffix, :target - AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) + AttributeMethodMatch = Struct.new(:target, :attr_name) def initialize(options = {}) @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "") @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/ - @method_missing_target = "#{@prefix}attribute#{@suffix}" + @target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" end def match(method_name) if @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $1, method_name) + AttributeMethodMatch.new(target, $1) end end diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index 6abf37bd44..d8cd48a53b 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true require "active_support/core_ext/hash/indifferent_access" +require "active_support/core_ext/object/duplicable" module ActiveModel class AttributeMutationTracker # :nodoc: OPTION_NOT_GIVEN = Object.new - def initialize(attributes) + def initialize(attributes, forced_changes = Set.new) @attributes = attributes - @forced_changes = Set.new + @forced_changes = forced_changes end def changed_attribute_names @@ -18,24 +19,22 @@ module ActiveModel 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 + result[attr_name] = original_value(attr_name) 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 + if change = change_to_attribute(attr_name) result.merge!(attr_name => change) end end end def change_to_attribute(attr_name) - attr_name = attr_name.to_s if changed?(attr_name) - [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + [original_value(attr_name), fetch_value(attr_name)] end end @@ -44,29 +43,26 @@ module ActiveModel 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) + attribute_changed?(attr_name) && + (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) && + (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to) end def changed_in_place?(attr_name) - attributes[attr_name.to_s].changed_in_place? + attributes[attr_name].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 + attributes[attr_name].original_value end def force_change(attr_name) - forced_changes << attr_name.to_s + forced_changes << attr_name end private @@ -75,45 +71,108 @@ module ActiveModel def attr_names attributes.keys end + + def attribute_changed?(attr_name) + forced_changes.include?(attr_name) || !!attributes[attr_name].changed? + end + + def fetch_value(attr_name) + attributes.fetch_value(attr_name) + end + end + + class ForcedMutationTracker < AttributeMutationTracker # :nodoc: + def initialize(attributes, forced_changes = {}) + super + @finalized_changes = nil + end + + def changed_in_place?(attr_name) + false + end + + def change_to_attribute(attr_name) + if finalized_changes&.include?(attr_name) + finalized_changes[attr_name].dup + else + super + end + end + + def forget_change(attr_name) + forced_changes.delete(attr_name) + end + + def original_value(attr_name) + if changed?(attr_name) + forced_changes[attr_name] + else + fetch_value(attr_name) + end + end + + def force_change(attr_name) + forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name) + end + + def finalize_changes + @finalized_changes = changes + end + + private + attr_reader :finalized_changes + + def attr_names + forced_changes.keys + end + + def attribute_changed?(attr_name) + forced_changes.include?(attr_name) + end + + def fetch_value(attr_name) + attributes.send(:_read_attribute, attr_name) + end + + def clone_value(attr_name) + value = fetch_value(attr_name) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value + end end class NullMutationTracker # :nodoc: include Singleton - def changed_attribute_names(*) + def changed_attribute_names [] end - def changed_values(*) + def changed_values {} end - def changes(*) + def changes {} end def change_to_attribute(attr_name) end - def any_changes?(*) + def any_changes? false end - def changed?(*) + def changed?(attr_name, **) false end - def changed_in_place?(*) + def changed_in_place?(attr_name) false end - def forget_change(*) - end - - def original_value(*) - end - - def force_change(*) + def original_value(attr_name) end end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index c3a446098c..6048ff0113 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -26,6 +26,21 @@ module ActiveModel define_attribute_method(name) end + # Returns an array of attribute names as strings + # + # class Person + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # Person.attribute_names + # # => ["name", "age"] + def attribute_names + attribute_types.keys + end + private def define_method_attribute=(name) @@ -65,10 +80,39 @@ module ActiveModel super end + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + # + # class Person + # include ActiveModel::Model + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # person = Person.new(name: 'Francesco', age: 22) + # person.attributes + # # => {"name"=>"Francesco", "age"=>22} def attributes @attributes.to_hash end + # Returns an array of attribute names as strings + # + # class Person + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # person = Person.new + # person.attribute_names + # # => ["name", "age"] + def attribute_names + @attributes.keys + end + private def write_attribute(attr_name, value) @@ -91,7 +135,7 @@ module ActiveModel @attributes.fetch_value(name) end - # Handle *= for method_missing. + # Dispatch target for <tt>*=</tt> attribute methods. def attribute=(attribute_name, value) write_attribute(attribute_name, value) end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0d9e761b1e..35a587658c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,7 +1,5 @@ # 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 @@ -122,9 +120,6 @@ module ActiveModel extend ActiveSupport::Concern include ActiveModel::AttributeMethods - OPTION_NOT_GIVEN = Object.new # :nodoc: - private_constant :OPTION_NOT_GIVEN - included do attribute_method_suffix "_changed?", "_change", "_will_change!", "_was" attribute_method_suffix "_previously_changed?", "_previous_change" @@ -145,10 +140,9 @@ module ActiveModel # +mutations_from_database+ to +mutations_before_last_save+ respectively. def changes_applied unless defined?(@attributes) - @previously_changed = changes + mutations_from_database.finalize_changes end @mutations_before_last_save = mutations_from_database - @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -159,7 +153,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.present? + mutations_from_database.any_changes? end # Returns an array with the name of the attributes with unsaved changes. @@ -168,42 +162,37 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - changed_attributes.keys + mutations_from_database.changed_attribute_names 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]) + # Dispatch target for <tt>*_changed?</tt> attribute methods. + def attribute_changed?(attr_name, **options) # :nodoc: + mutations_from_database.changed?(attr_name.to_s, options) end - # Handles <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + # Dispatch target for <tt>*_was</tt> attribute methods. + def attribute_was(attr_name) # :nodoc: + mutations_from_database.original_value(attr_name.to_s) end - # Handles <tt>*_previously_changed?</tt> for +method_missing+. - def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) + # Dispatch target for <tt>*_previously_changed?</tt> attribute methods. + def attribute_previously_changed?(attr_name) # :nodoc: + mutations_before_last_save.changed?(attr_name.to_s) end # Restore all previous data of the provided attributes. - def restore_attributes(attributes = changed) - attributes.each { |attr| restore_attribute! attr } + def restore_attributes(attr_names = changed) + attr_names.each { |attr_name| restore_attribute!(attr_name) } 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 @@ -216,13 +205,7 @@ module ActiveModel # 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 + mutations_from_database.changed_values end # Returns a hash of changed attributes indicating their original @@ -232,9 +215,7 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - cache_changed_attributes do - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] - end + mutations_from_database.changes end # Returns a hash of attributes that were changed before the model was saved. @@ -244,27 +225,23 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new - @previously_changed.merge(mutations_before_last_save.changes) + mutations_before_last_save.changes end def attribute_changed_in_place?(attr_name) # :nodoc: - mutations_from_database.changed_in_place?(attr_name) + mutations_from_database.changed_in_place?(attr_name.to_s) end private def clear_attribute_change(attr_name) - mutations_from_database.forget_change(attr_name) + mutations_from_database.forget_change(attr_name.to_s) 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 + ActiveModel::ForcedMutationTracker.new(self) end end @@ -276,68 +253,28 @@ module ActiveModel @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end - 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) || mutations_from_database.changed?(attr_name) - end - alias attribute_changed_by_setter? changes_include? - - # Returns +true+ if attr_name were changed before the model was saved, - # +false+ otherwise. - def previous_changes_include?(attr_name) - previous_changes.include?(attr_name) - end - - # Handles <tt>*_change</tt> for +method_missing+. - def attribute_change(attr) - [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) + # Dispatch target for <tt>*_change</tt> attribute methods. + def attribute_change(attr_name) + mutations_from_database.change_to_attribute(attr_name.to_s) end - # Handles <tt>*_previous_change</tt> for +method_missing+. - def attribute_previous_change(attr) - previous_changes[attr] + # Dispatch target for <tt>*_previous_change</tt> attribute methods. + def attribute_previous_change(attr_name) + mutations_before_last_save.change_to_attribute(attr_name.to_s) end - # Handles <tt>*_will_change!</tt> for +method_missing+. - def attribute_will_change!(attr) - unless attribute_changed?(attr) - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - end - - set_attribute_was(attr, value) - end - mutations_from_database.force_change(attr) + # Dispatch target for <tt>*_will_change!</tt> attribute methods. + def attribute_will_change!(attr_name) + mutations_from_database.force_change(attr_name.to_s) end - # Handles <tt>restore_*!</tt> for +method_missing+. - def restore_attribute!(attr) - if attribute_changed?(attr) - __send__("#{attr}=", changed_attributes[attr]) - clear_attribute_changes([attr]) + # Dispatch target for <tt>restore_*!</tt> attribute methods. + def restore_attribute!(attr_name) + attr_name = attr_name.to_s + if attribute_changed?(attr_name) + __send__("#{attr_name}=", attribute_was(attr_name)) + clear_attribute_change(attr_name) end end - - 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 end end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index ebb6cc542d..4e228032c3 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -264,6 +264,5 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "foo", match.attr_name assert_equal "attribute_test", match.target - assert_equal "foo_test", match.method_name end end diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb index 5483fb101d..af0ddcb92f 100644 --- a/activemodel/test/cases/attributes_test.rb +++ b/activemodel/test/cases/attributes_test.rb @@ -67,6 +67,20 @@ module ActiveModel assert_equal expected_attributes, data.attributes end + test "reading attribute names" do + names = [ + "integer_field", + "string_field", + "decimal_field", + "string_with_default", + "date_field", + "boolean_field" + ] + + assert_equal names, ModelForAttributesTest.attribute_names + assert_equal names, ModelForAttributesTest.new.attribute_names + end + test "nonexistent attribute" do assert_raise ActiveModel::UnknownAttributeError do ModelForAttributesTest.new(nonexistent: "nonexistent") diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index 7662f996ae..72baf6e7a7 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -7,21 +7,23 @@ require "models/reply" require "models/person" class AcceptanceValidationTest < ActiveModel::TestCase - def teardown - Topic.clear_validators! + teardown do + self.class.send(:remove_const, :TestClass) end def test_terms_of_service_agreement_no_acceptance - Topic.validates_acceptance_of(:terms_of_service) + klass = define_test_class(Topic) + klass.validates_acceptance_of(:terms_of_service) - t = Topic.new("title" => "We should not be confirmed") + t = klass.new("title" => "We should not be confirmed") assert_predicate t, :valid? end def test_terms_of_service_agreement - Topic.validates_acceptance_of(:terms_of_service) + klass = define_test_class(Topic) + klass.validates_acceptance_of(:terms_of_service) - t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") + t = klass.new("title" => "We should be confirmed", "terms_of_service" => "") assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] @@ -30,9 +32,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase end def test_eula - Topic.validates_acceptance_of(:eula, message: "must be abided") + klass = define_test_class(Topic) + klass.validates_acceptance_of(:eula, message: "must be abided") - t = Topic.new("title" => "We should be confirmed", "eula" => "") + t = klass.new("title" => "We should be confirmed", "eula" => "") assert_predicate t, :invalid? assert_equal ["must be abided"], t.errors[:eula] @@ -41,9 +44,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase end def test_terms_of_service_agreement_with_accept_value - Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.") + klass = define_test_class(Topic) + klass.validates_acceptance_of(:terms_of_service, accept: "I agree.") - t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") + t = klass.new("title" => "We should be confirmed", "terms_of_service" => "") assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] @@ -52,9 +56,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase end def test_terms_of_service_agreement_with_multiple_accept_values - Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) + klass = define_test_class(Topic) + klass.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) - t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") + t = klass.new("title" => "We should be confirmed", "terms_of_service" => "") assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] @@ -66,9 +71,10 @@ class AcceptanceValidationTest < ActiveModel::TestCase end def test_validates_acceptance_of_for_ruby_class - Person.validates_acceptance_of :karma + klass = define_test_class(Person) + klass.validates_acceptance_of :karma - p = Person.new + p = klass.new p.karma = "" assert_predicate p, :invalid? @@ -76,13 +82,21 @@ class AcceptanceValidationTest < ActiveModel::TestCase p.karma = "1" assert_predicate p, :valid? - ensure - Person.clear_validators! end def test_validates_acceptance_of_true - Topic.validates_acceptance_of(:terms_of_service) + klass = define_test_class(Topic) + klass.validates_acceptance_of(:terms_of_service) - assert_predicate Topic.new(terms_of_service: true), :valid? + assert_predicate klass.new(terms_of_service: true), :valid? end + + private + + # Acceptance validator includes anonymous module into class, which cannot + # be cleared, so to avoid multiple inclusions we use a named subclass which + # we can remove in teardown. + def define_test_class(parent) + self.class.const_set(:TestClass, Class.new(parent)) + end end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index eb03e837f1..35bb918f26 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -6,7 +6,7 @@ require "models/person" class I18nValidationTest < ActiveModel::TestCase def setup Person.clear_validators! - @person = Person.new + @person = person_class.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend I18n.load_path.clear @@ -18,7 +18,9 @@ class I18nValidationTest < ActiveModel::TestCase end def teardown - Person.clear_validators! + person_class.clear_validators! + self.class.send(:remove_const, :Person) + @person_stub = nil I18n.load_path.replace @old_load_path I18n.backend = @old_backend I18n.backend.reload! @@ -28,14 +30,14 @@ class I18nValidationTest < ActiveModel::TestCase def test_full_message_encoding I18n.backend.store_translations("en", errors: { messages: { too_short: "猫舌" } }) - Person.validates_length_of :title, within: 3..5 + person_class.validates_length_of :title, within: 3..5 @person.valid? assert_equal ["Title 猫舌"], @person.errors.full_messages end def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, "not found") - assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do + assert_called_with(person_class, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do assert_equal ["Person's name not found"], @person.errors.full_messages end end @@ -52,7 +54,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } }) - person = Person.new + person = person_class.new assert_equal "Name cannot be blank", person.errors.full_message(:name, "cannot be blank") assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank") end @@ -63,7 +65,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank") assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank") end @@ -74,7 +76,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { person: { format: "%{message}" } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:name, "cannot be blank") assert_equal "cannot be blank", person.errors.full_message(:name_test, "cannot be blank") end @@ -85,7 +87,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank") assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank") end @@ -96,7 +98,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank") assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank") end @@ -107,7 +109,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") end @@ -118,7 +120,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } }) - person = Person.new + person = person_class.new assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") end @@ -130,7 +132,7 @@ class I18nValidationTest < ActiveModel::TestCase attributes: { 'person/contacts/addresses': { country: "Country" } } }) - person = Person.new + person = person_class.new assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") end @@ -141,7 +143,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.backend.store_translations("en", activemodel: { errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) - person = Person.new + person = person_class.new assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") end @@ -153,7 +155,7 @@ class I18nValidationTest < ActiveModel::TestCase attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } } }) - person = Person.new + person = person_class.new assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") end @@ -174,7 +176,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_confirmation_of on generated message #{name}" do - Person.validates_confirmation_of :title, validation_options + person_class.validates_confirmation_of :title, validation_options @person.title_confirmation = "foo" call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")] assert_called_with(@person.errors, :generate_message, call) do @@ -185,7 +187,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_acceptance_of on generated message #{name}" do - Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false) + person_class.validates_acceptance_of :title, validation_options.merge(allow_nil: false) call = [:title, :accepted, generate_message_options] assert_called_with(@person.errors, :generate_message, call) do @person.valid? @@ -195,7 +197,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_presence_of on generated message #{name}" do - Person.validates_presence_of :title, validation_options + person_class.validates_presence_of :title, validation_options call = [:title, :blank, generate_message_options] assert_called_with(@person.errors, :generate_message, call) do @person.valid? @@ -205,7 +207,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :within on generated message when too short #{name}" do - Person.validates_length_of :title, validation_options.merge(within: 3..5) + person_class.validates_length_of :title, validation_options.merge(within: 3..5) call = [:title, :too_short, generate_message_options.merge(count: 3)] assert_called_with(@person.errors, :generate_message, call) do @person.valid? @@ -215,7 +217,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :too_long generated message #{name}" do - Person.validates_length_of :title, validation_options.merge(within: 3..5) + person_class.validates_length_of :title, validation_options.merge(within: 3..5) @person.title = "this title is too long" call = [:title, :too_long, generate_message_options.merge(count: 5)] assert_called_with(@person.errors, :generate_message, call) do @@ -226,7 +228,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_length_of for :is on generated message #{name}" do - Person.validates_length_of :title, validation_options.merge(is: 5) + person_class.validates_length_of :title, validation_options.merge(is: 5) call = [:title, :wrong_length, generate_message_options.merge(count: 5)] assert_called_with(@person.errors, :generate_message, call) do @person.valid? @@ -236,7 +238,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do - Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/) + person_class.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/) @person.title = "72x" call = [:title, :invalid, generate_message_options.merge(value: "72x")] assert_called_with(@person.errors, :generate_message, call) do @@ -247,7 +249,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of on generated message #{name}" do - Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c)) + person_class.validates_inclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = "z" call = [:title, :inclusion, generate_message_options.merge(value: "z")] assert_called_with(@person.errors, :generate_message, call) do @@ -258,7 +260,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_inclusion_of using :within on generated message #{name}" do - Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c)) + person_class.validates_inclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = "z" call = [:title, :inclusion, generate_message_options.merge(value: "z")] assert_called_with(@person.errors, :generate_message, call) do @@ -269,7 +271,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of generated message #{name}" do - Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) + person_class.validates_exclusion_of :title, validation_options.merge(in: %w(a b c)) @person.title = "a" call = [:title, :exclusion, generate_message_options.merge(value: "a")] assert_called_with(@person.errors, :generate_message, call) do @@ -280,7 +282,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_exclusion_of using :within generated message #{name}" do - Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c)) + person_class.validates_exclusion_of :title, validation_options.merge(within: %w(a b c)) @person.title = "a" call = [:title, :exclusion, generate_message_options.merge(value: "a")] assert_called_with(@person.errors, :generate_message, call) do @@ -291,7 +293,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of generated message #{name}" do - Person.validates_numericality_of :title, validation_options + person_class.validates_numericality_of :title, validation_options @person.title = "a" call = [:title, :not_a_number, generate_message_options.merge(value: "a")] assert_called_with(@person.errors, :generate_message, call) do @@ -302,7 +304,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :only_integer on generated message #{name}" do - Person.validates_numericality_of :title, validation_options.merge(only_integer: true) + person_class.validates_numericality_of :title, validation_options.merge(only_integer: true) @person.title = "0.0" call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")] assert_called_with(@person.errors, :generate_message, call) do @@ -313,7 +315,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :odd on generated message #{name}" do - Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true) + person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true) @person.title = 0 call = [:title, :odd, generate_message_options.merge(value: 0)] assert_called_with(@person.errors, :generate_message, call) do @@ -324,7 +326,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_numericality_of for :less_than on generated message #{name}" do - Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0) + person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0) @person.title = 1 call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)] assert_called_with(@person.errors, :generate_message, call) do @@ -369,67 +371,67 @@ class I18nValidationTest < ActiveModel::TestCase end set_expectations_for_validation "validates_confirmation_of", :confirmation do |person, options_to_merge| - Person.validates_confirmation_of :title, options_to_merge + person.class.validates_confirmation_of :title, options_to_merge person.title_confirmation = "foo" end set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge| - Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false) + person.class.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false) end set_expectations_for_validation "validates_presence_of", :blank do |person, options_to_merge| - Person.validates_presence_of :title, options_to_merge + person.class.validates_presence_of :title, options_to_merge end set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge| - Person.validates_length_of :title, options_to_merge.merge(within: 3..5) + person.class.validates_length_of :title, options_to_merge.merge(within: 3..5) end set_expectations_for_validation "validates_length_of", :too_long do |person, options_to_merge| - Person.validates_length_of :title, options_to_merge.merge(within: 3..5) + person.class.validates_length_of :title, options_to_merge.merge(within: 3..5) person.title = "too long" end set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge| - Person.validates_length_of :title, options_to_merge.merge(is: 5) + person.class.validates_length_of :title, options_to_merge.merge(is: 5) end set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| - Person.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/) + person.class.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/) end set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge| - Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c)) + person.class.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c)) end set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge| - Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c)) + person.class.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c)) person.title = "a" end set_expectations_for_validation "validates_numericality_of", :not_a_number do |person, options_to_merge| - Person.validates_numericality_of :title, options_to_merge + person.class.validates_numericality_of :title, options_to_merge person.title = "a" end set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge| - Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true) + person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true) person.title = "1.0" end set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge| - Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true) + person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true) person.title = 0 end set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge| - Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0) + person.class.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0) person.title = 1 end def test_validations_with_message_symbol_must_translate I18n.backend.store_translations "en", errors: { messages: { custom_error: "I am a custom error" } } - Person.validates_presence_of :title, message: :custom_error + person_class.validates_presence_of :title, message: :custom_error @person.title = nil @person.valid? assert_equal ["I am a custom error"], @person.errors[:title] @@ -437,7 +439,7 @@ class I18nValidationTest < ActiveModel::TestCase def test_validates_with_message_symbol_must_translate_per_attribute I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } } - Person.validates_presence_of :title, message: :custom_error + person_class.validates_presence_of :title, message: :custom_error @person.title = nil @person.valid? assert_equal ["I am a custom error"], @person.errors[:title] @@ -445,16 +447,20 @@ class I18nValidationTest < ActiveModel::TestCase def test_validates_with_message_symbol_must_translate_per_model I18n.backend.store_translations "en", activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } } - Person.validates_presence_of :title, message: :custom_error + person_class.validates_presence_of :title, message: :custom_error @person.title = nil @person.valid? assert_equal ["I am a custom error"], @person.errors[:title] end def test_validates_with_message_string - Person.validates_presence_of :title, message: "I am a custom error" + person_class.validates_presence_of :title, message: "I am a custom error" @person.title = nil @person.valid? assert_equal ["I am a custom error"], @person.errors[:title] end + + def person_class + @person_stub ||= self.class.const_set(:Person, Class.new(Person)) + end end |