diff options
Diffstat (limited to 'activemodel')
90 files changed, 1555 insertions, 882 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 909fbe512b..55e0c85b00 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,7 +1,5 @@ -## Rails 5.2.0.beta2 (November 28, 2017) ## - -* Return correct date while converting parameters in `value_from_multiparameter_assignment` - for `ActiveModel::Type::Date` +* Fix date value when casting a multiparameter date hash to not convert + from Gregorian date to Julian date. Before: @@ -13,59 +11,103 @@ Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"}) => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil> - Fixes #28521 + Fixes #28521. *Sayan Chakraborty* +* Fix year value when casting a multiparameter time hash. + + When assigning a hash to a time attribute that's missing a year component + (e.g. a `time_select` with `:ignore_date` set to `true`) then the year + defaults to 1970 instead of the expected 2000. This results in the attribute + changing as a result of the save. + + Before: + ``` + event = Event.new(start_time: { 4 => 20, 5 => 30 }) + event.start_time # => 1970-01-01 20:30:00 UTC + event.save + event.reload + event.start_time # => 2000-01-01 20:30:00 UTC + ``` + + After: + ``` + event = Event.new(start_time: { 4 => 20, 5 => 30 }) + event.start_time # => 2000-01-01 20:30:00 UTC + event.save + event.reload + event.start_time # => 2000-01-01 20:30:00 UTC + ``` -## Rails 5.2.0.beta1 (November 27, 2017) ## + *Andrew White* -* Execute `ConfirmationValidator` validation when `_confirmation`'s value is `false`. - *bogdanvlviv* +## Rails 6.0.0.beta1 (January 18, 2019) ## -* Allow passing a Proc or Symbol to length validator options. +* Add `ActiveModel::Errors#of_kind?`. - *Matt Rohrer* + *bogdanvlviv*, *Rafael Mendonça França* -* Add method `#merge!` for `ActiveModel::Errors`. +* Fix numericality equality validation of `BigDecimal` and `Float` + by casting to `BigDecimal` on both ends of the validation. - *Jahfer Husain* + *Gannon McGibbon* -* Fix regression in numericality validator when comparing Decimal and Float input - values with more scale than the schema. +* Add `#slice!` method to `ActiveModel::Errors`. - *Bradley Priest* + *Daniel López Prat* -* Fix methods `#keys`, `#values` in `ActiveModel::Errors`. +* Fix numericality validator to still use value before type cast except Active Record. - Change `#keys` to only return the keys that don't have empty messages. + Fixes #33651, #33686. - Change `#values` to only return the not empty values. + *Ryuta Kamizono* + +* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps. + + Before: + ``` + contact = Contact.new(created_at: Time.utc(2006, 8, 1)) + contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC + ``` + + After: + ``` + contact = Contact.new(created_at: Time.utc(2006, 8, 1)) + contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z" + ``` + + *Bogdan Gusiev* + +* Allows configurable attribute name for `#has_secure_password`. This + still defaults to an attribute named 'password', causing no breaking + change. There is a new method `#authenticate_XXX` where XXX is the + configured attribute name, making the existing `#authenticate` now an + alias for this when the attribute is the default 'password'. Example: - # 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. + class User < ActiveRecord::Base + has_secure_password :recovery_password, validations: false + end + + user = User.new() + user.recovery_password = "42password" + user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..." + user.authenticate_recovery_password('42password') # => user + + *Unathi Chonco* + +* Add `config.active_model.i18n_full_message` in order to control whether + the `full_message` error format can be overridden at the attribute or model + level in the locale files. This is `false` by default. + + *Martin Larochelle* + +* Rails 6 requires Ruby 2.5.0 or newer. + + *Jeremy Daer*, *Kasper Timm Hansen* + + +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index ac810e86d0..ab7c27c209 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index a070a2898c..4deb76814b 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "A toolkit for building modeling frameworks (part of Rails)." s.description = "A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.5.0" s.license = "MIT" @@ -25,5 +25,8 @@ Gem::Specification.new do |s| "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md" } + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + s.add_dependency "activesupport", version end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index a3884206a6..c9140dc582 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb index b75ff80b31..75f60d205e 100644 --- a/activemodel/lib/active_model/attribute.rb +++ b/activemodel/lib/active_model/attribute.rb @@ -133,10 +133,6 @@ module ActiveModel end protected - - attr_reader :original_attribute - alias_method :assigned?, :original_attribute - def original_value_for_database if assigned? original_attribute.original_value_for_database @@ -146,6 +142,9 @@ module ActiveModel end private + attr_reader :original_attribute + alias :assigned? :original_attribute + def initialize_dup(other) if defined?(@value) && @value.duplicable? @value = @value.dup @@ -207,6 +206,7 @@ module ActiveModel raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" end alias_method :with_value_from_user, :with_value_from_database + alias_method :with_cast_value, :with_value_from_database end class Uninitialized < Attribute # :nodoc: @@ -233,6 +233,10 @@ module ActiveModel false end + def forgetting_assignment + dup + end + def with_type(type) self.class.new(name, type) end diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb index f274b687d4..9dc16e882d 100644 --- a/activemodel/lib/active_model/attribute/user_provided_default.rb +++ b/activemodel/lib/active_model/attribute/user_provided_default.rb @@ -22,8 +22,29 @@ module ActiveModel self.class.new(name, user_provided_value, type, original_attribute) end - protected + def marshal_dump + result = [ + name, + value_before_type_cast, + type, + original_attribute, + ] + result << value if defined?(@value) + result + end + + def marshal_load(values) + name, user_provided_value, type, original_attribute, value = values + @name = name + @user_provided_value = user_provided_value + @type = type + @original_attribute = original_attribute + if values.length == 5 + @value = value + end + end + private attr_reader :user_provided_value end end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index aa931119ff..f0e3458f51 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -27,7 +27,7 @@ module ActiveModel # cat.status # => 'sleeping' def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) - raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed." end return if new_attributes.empty? @@ -35,6 +35,8 @@ module ActiveModel _assign_attributes(sanitize_for_mass_assignment(attributes)) end + alias attributes= assign_attributes + private def _assign_attributes(attributes) diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 888a431e5f..5c4670f393 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -369,7 +369,7 @@ module ActiveModel "define_method(:'#{name}') do |*args|" end - extra = (extra.map!(&:inspect) << "*args").join(", ".freeze) + extra = (extra.map!(&:inspect) << "*args").join(", ") target = if CALL_COMPILABLE_REGEXP.match?(send) "#{"self." unless include_private}#{send}(#{extra})" @@ -474,5 +474,43 @@ module ActiveModel def _read_attribute(attr) __send__(attr) end + + module AttrNames # :nodoc: + DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/ + + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes in read_attribute. + def self.define_attribute_accessor_method(mod, attr_name, writer: false) + method_name = "#{attr_name}#{'=' if writer}" + if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name) + yield method_name, "'#{attr_name}'.freeze" + else + safe_name = attr_name.unpack1("h*") + const_name = "ATTR_#{safe_name}" + const_set(const_name, attr_name) unless const_defined?(const_name) + temp_method_name = "__temp__#{safe_name}#{'=' if writer}" + attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}" + yield temp_method_name, attr_name_expr + mod.alias_method method_name, temp_method_name + mod.undef_method temp_method_name + end + end + end end end diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index c67e1b809a..6abf37bd44 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -11,6 +11,10 @@ module ActiveModel @forced_changes = Set.new end + def changed_attribute_names + attr_names.select { |attr_name| changed?(attr_name) } + end + def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) @@ -23,7 +27,7 @@ module ActiveModel attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| change = change_to_attribute(attr_name) if change - result[attr_name] = change + result.merge!(attr_name => change) end end end @@ -65,13 +69,8 @@ module ActiveModel 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 + attr_reader :attributes, :forced_changes def attr_names attributes.keys @@ -81,6 +80,10 @@ module ActiveModel class NullMutationTracker # :nodoc: include Singleton + def changed_attribute_names(*) + [] + end + def changed_values(*) {} end diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb index 54a5dd4064..4679b33852 100644 --- a/activemodel/lib/active_model/attribute_set.rb +++ b/activemodel/lib/active_model/attribute_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set/builder" require "active_model/attribute_set/yaml_encoder" @@ -36,16 +37,8 @@ module ActiveModel attributes.each_key.select { |name| self[name].initialized? } end - if defined?(JRUBY_VERSION) - # This form is significantly faster on JRuby, and this is one of our biggest hotspots. - # https://github.com/jruby/jruby/pull/2562 - def fetch_value(name, &block) - self[name].value(&block) - end - else - def fetch_value(name) - self[name].value { |n| yield n if block_given? } - end + def fetch_value(name, &block) + self[name].value(&block) end def write_from_database(name, value) diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb index 758eb830fc..2b1c2206ec 100644 --- a/activemodel/lib/active_model/attribute_set/builder.rb +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -22,12 +22,12 @@ module ActiveModel class LazyAttributeHash # :nodoc: delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize - def initialize(types, values, additional_types, default_attributes) + def initialize(types, values, additional_types, default_attributes, delegate_hash = {}) @types = types @values = values @additional_types = additional_types @materialized = false - @delegate_hash = {} + @delegate_hash = delegate_hash @default_attributes = default_attributes end @@ -76,21 +76,20 @@ module ActiveModel end def marshal_dump - materialize + [@types, @values, @additional_types, @default_attributes, @delegate_hash] end - def marshal_load(delegate_hash) - @delegate_hash = delegate_hash - @types = {} - @values = {} - @additional_types = {} - @materialized = true + def marshal_load(values) + if values.is_a?(Hash) + empty_hash = {}.freeze + initialize(empty_hash, empty_hash, empty_hash, empty_hash, values) + @materialized = true + else + initialize(*values) + end end protected - - attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes - def materialize unless @materialized values.each_key { |key| self[key] } @@ -103,6 +102,7 @@ module ActiveModel end private + attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes def assign_default_value(name) type = additional_types.fetch(name, types[name]) diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb index 4ea945b956..ea1efc160e 100644 --- a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb +++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb @@ -33,8 +33,7 @@ module ActiveModel end end - protected - + private attr_reader :default_types end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index cac461b549..c3a446098c 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/object/deep_dup" require "active_model/attribute_set" require "active_model/attribute/user_provided_default" @@ -24,23 +23,22 @@ module ActiveModel end self.attribute_types = attribute_types.merge(name => type) define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type) - define_attribute_methods(name) + define_attribute_method(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 + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + write_attribute(name, value) + end + RUBY + end end NO_DEFAULT_PROVIDED = Object.new # :nodoc: @@ -67,6 +65,10 @@ module ActiveModel super end + def attributes + @attributes.to_hash + end + private def write_attribute(attr_name, value) @@ -94,15 +96,4 @@ module ActiveModel write_attribute(attribute_name, value) end end - - module AttributeMethods #:nodoc: - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, value.dup.freeze - end - end - } - end end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 8fa9680cb1..ea2ed7dff7 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/hash/keys" module ActiveModel # == Active \Model \Callbacks @@ -127,26 +128,28 @@ module ActiveModel private def _define_before_model_callback(klass, callback) - klass.define_singleton_method("before_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :before, *args, &block) + klass.define_singleton_method("before_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :before, *args, options, &block) end end def _define_around_model_callback(klass, callback) - klass.define_singleton_method("around_#{callback}") do |*args, &block| - set_callback(:"#{callback}", :around, *args, &block) + klass.define_singleton_method("around_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) + set_callback(:"#{callback}", :around, *args, options, &block) end end def _define_after_model_callback(klass, callback) - klass.define_singleton_method("after_#{callback}") do |*args, &block| - options = args.extract_options! + klass.define_singleton_method("after_#{callback}") do |*args, **options, &block| + options.assert_valid_keys(:if, :unless, :prepend) options[:prepend] = true conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v| v != false } options[:if] = Array(options[:if]) << conditional - set_callback(:"#{callback}", :after, *(args << options), &block) + set_callback(:"#{callback}", :after, *args, options, &block) end end end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index cdc1282817..82713ddc81 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -103,7 +103,7 @@ module ActiveModel @_to_partial_path ||= begin element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name)) collection = ActiveSupport::Inflector.tableize(name) - "#{collection}/#{element}".freeze + "#{collection}/#{element}" end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index d2ebd18107..0d9e761b1e 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -141,15 +141,19 @@ module ActiveModel @mutations_from_database = nil end - def changes_applied # :nodoc: - @previously_changed = changes + # Clears dirty data and moves +changes+ to +previously_changed+ and + # +mutations_from_database+ to +mutations_before_last_save+ respectively. + def changes_applied + unless defined?(@attributes) + @previously_changed = changes + end @mutations_before_last_save = mutations_from_database @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end - # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise. + # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise. # # person.changed? # => false # person.name = 'bob' @@ -302,7 +306,7 @@ module ActiveModel # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + previous_changes[attr] end # Handles <tt>*_will_change!</tt> for +method_missing+. diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 275e3f1313..9fd6f2d89c 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -62,6 +62,11 @@ module ActiveModel CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] MESSAGE_OPTIONS = [:message] + class << self + attr_accessor :i18n_full_message # :nodoc: + end + self.i18n_full_message = false + attr_reader :messages, :details # Pass in the instance of the object that is using the errors object. @@ -107,6 +112,17 @@ module ActiveModel @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 } end + # Removes all errors except the given keys. Returns a hash containing the removed errors. + # + # person.errors.keys # => [:name, :age, :gender, :city] + # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] } + # person.errors.keys # => [:age, :gender] + def slice!(*keys) + keys = keys.map(&:to_sym) + @details.slice!(*keys) + @messages.slice!(*keys) + end + # Clear the error messages. # # person.errors.full_messages # => ["name cannot be nil"] @@ -312,21 +328,42 @@ module ActiveModel # person.errors.added? :name, :blank # => true # person.errors.added? :name, "can't be blank" # => true # - # If the error message requires an option, then it returns +true+ with - # the correct option, or +false+ with an incorrect or missing option. + # If the error message requires options, then it returns +true+ with + # the correct options, or +false+ with incorrect or missing options. # - # person.errors.add :name, :too_long, { count: 25 } - # person.errors.added? :name, :too_long, count: 25 # => true - # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true - # person.errors.added? :name, :too_long, count: 24 # => false - # person.errors.added? :name, :too_long # => false - # person.errors.added? :name, "is too long" # => false + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.added? :name, :too_long, count: 25 # => true + # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.added? :name, :too_long, count: 24 # => false + # person.errors.added? :name, :too_long # => false + # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) + message = message.call if message.respond_to?(:call) + + if message.is_a? Symbol + details[attribute.to_sym].include? normalize_detail(message, options) + else + self[attribute].include? message + end + end + + # Returns +true+ if an error on the attribute with the given message is + # present, or +false+ otherwise. +message+ is treated the same as for +add+. + # + # person.errors.add :age + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.of_kind? :age # => true + # person.errors.of_kind? :name # => false + # person.errors.of_kind? :name, :too_long # => true + # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.of_kind? :name, :not_too_long # => false + # person.errors.of_kind? :name, "is too long" # => false + def of_kind?(attribute, message = :invalid) + message = message.call if message.respond_to?(:call) + if message.is_a? Symbol - self.details[attribute].map { |e| e[:error] }.include? message + details[attribute.to_sym].map { |e| e[:error] }.include? message else - message = message.call if message.respond_to?(:call) - message = normalize_message(attribute, message, options) self[attribute].include? message end end @@ -364,12 +401,54 @@ module ActiveModel # Returns a full message for a given attribute. # # person.errors.full_message(:name, 'is invalid') # => "Name is invalid" + # + # The `"%{attribute} %{message}"` error format can be overridden with either + # + # * <tt>activemodel.errors.models.person/contacts/addresses.attributes.street.format</tt> + # * <tt>activemodel.errors.models.person/contacts/addresses.format</tt> + # * <tt>activemodel.errors.models.person.attributes.name.format</tt> + # * <tt>activemodel.errors.models.person.format</tt> + # * <tt>errors.format</tt> def full_message(attribute, message) return message if attribute == :base - attr_name = attribute.to_s.tr(".", "_").humanize + attribute = attribute.to_s + + if self.class.i18n_full_message && @base.class.respond_to?(:i18n_scope) + attribute = attribute.remove(/\[\d\]/) + parts = attribute.split(".") + attribute_name = parts.pop + namespace = parts.join("/") unless parts.empty? + attributes_scope = "#{@base.class.i18n_scope}.errors.models" + + if namespace + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format", + ] + end + else + defaults = @base.class.lookup_ancestors.map do |klass| + [ + :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format", + :"#{attributes_scope}.#{klass.model_name.i18n_key}.format", + ] + end + end + + defaults.flatten! + else + defaults = [] + end + + defaults << :"errors.format" + defaults << "%{attribute} %{message}" + + attr_name = attribute.tr(".", "_").humanize attr_name = @base.class.human_attribute_name(attribute, default: attr_name) - I18n.t(:"errors.format", - default: "%{attribute} %{message}", + + I18n.t(defaults.shift, + default: defaults, attribute: attr_name, message: message) end diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 3c344fe854..b0322f62c7 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -7,10 +7,10 @@ module ActiveModel end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "beta1" 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 34d9ac6c96..b7ceabb59a 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -29,7 +29,7 @@ module ActiveModel # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes # of the model, and is used to a generate unique DOM id for the object. def test_to_key - assert model.respond_to?(:to_key), "The model should respond to to_key" + assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end @@ -44,7 +44,7 @@ module ActiveModel # tests for this behavior in lint because it doesn't make sense to force # any of the possible implementation strategies on the implementer. def test_to_param - assert model.respond_to?(:to_param), "The model should respond to to_param" + assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" @@ -56,7 +56,7 @@ module ActiveModel # <tt>to_partial_path</tt> is used for looking up partials. For example, # a BlogPost model might return "blog_posts/blog_post". def test_to_partial_path - assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" + assert_respond_to model, :to_partial_path assert_kind_of String, model.to_partial_path end @@ -68,7 +68,7 @@ module ActiveModel # will route to the create action. If it is persisted, a form for the # object will route to the update action. def test_persisted? - assert model.respond_to?(:persisted?), "The model should respond to persisted?" + assert_respond_to model, :persisted? assert_boolean model.persisted?, "persisted?" end @@ -79,14 +79,14 @@ module ActiveModel # # Check ActiveModel::Naming for more information. def test_model_naming - assert model.class.respond_to?(:model_name), "The model class should respond to model_name" + assert_respond_to model.class, :model_name model_name = model.class.model_name - assert model_name.respond_to?(:to_str) - assert model_name.human.respond_to?(:to_str) - assert model_name.singular.respond_to?(:to_str) - assert model_name.plural.respond_to?(:to_str) + assert_respond_to model_name, :to_str + assert_respond_to model_name.human, :to_str + assert_respond_to model_name.singular, :to_str + assert_respond_to model_name.plural, :to_str - assert model.respond_to?(:model_name), "The model instance should respond to model_name" + assert_respond_to model, :model_name assert_equal model.model_name, model.class.model_name end @@ -100,13 +100,13 @@ module ActiveModel # If localization is used, the strings should be localized for the current # locale. If no error is present, the method should return an empty array. def test_errors_aref - assert model.respond_to?(:errors), "The model should respond to errors" + assert_respond_to model, :errors assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end private def model - assert @model.respond_to?(:to_model), "The object should respond to to_model" + assert_respond_to @model, :to_model @model.to_model end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index dfccd03cd8..bf23fa3c05 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -111,6 +111,22 @@ module ActiveModel # BlogPost.model_name.eql?('Blog Post') # => false ## + # :method: match? + # + # :call-seq: + # match?(regexp) + # + # Equivalent to <tt>String#match?</tt>. Match the class name against the + # given regexp. Returns +true+ if there is a match, otherwise +false+. + # + # class BlogPost + # extend ActiveModel::Naming + # end + # + # BlogPost.model_name.match?(/Post/) # => true + # BlogPost.model_name.match?(/\d/) # => false + + ## # :method: to_s # # :call-seq: @@ -131,7 +147,7 @@ module ActiveModel # to_str() # # Equivalent to +to_s+. - delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s, + delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s, :to_str, :as_json, to: :name # Returns a new ActiveModel::Name instance. By default, the +namespace+ @@ -193,7 +209,7 @@ module ActiveModel private def _singularize(string) - ActiveSupport::Inflector.underscore(string).tr("/".freeze, "_".freeze) + ActiveSupport::Inflector.underscore(string).tr("/", "_") end end @@ -236,7 +252,7 @@ module ActiveModel # Person.model_name.plural # => "people" def model_name @_model_name ||= begin - namespace = parents.detect do |n| + namespace = module_parents.detect do |n| n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? end ActiveModel::Name.new(self, namespace) diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index a9cdabba00..0ed70bd473 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -7,8 +7,14 @@ module ActiveModel class Railtie < Rails::Railtie # :nodoc: config.eager_load_namespaces << ActiveModel + config.active_model = ActiveSupport::OrderedOptions.new + initializer "active_model.secure_password" do ActiveModel::SecurePassword.min_cost = Rails.env.test? end + + initializer "active_model.i18n_full_message" do + ActiveModel::Errors.i18n_full_message = config.active_model.delete(:i18n_full_message) || false + end end end diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 86f051f5ce..51d54f34f3 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -16,15 +16,16 @@ module ActiveModel module ClassMethods # Adds methods to set and authenticate against a BCrypt password. - # This mechanism requires you to have a +password_digest+ attribute. + # This mechanism requires you to have a +XXX_digest+ attribute. + # Where +XXX+ is the attribute name of your desired password. # # The following validations are added automatically: # * Password must be present on creation # * Password length should be less than or equal to 72 bytes - # * Confirmation of password (using a +password_confirmation+ attribute) + # * Confirmation of password (using a +XXX_confirmation+ attribute) # - # If password confirmation validation is not needed, simply leave out the - # value for +password_confirmation+ (i.e. don't provide a form field for + # If confirmation validation is not needed, simply leave out the + # value for +XXX_confirmation+ (i.e. don't provide a form field for # it). When this attribute has a +nil+ value, the validation will not be # triggered. # @@ -37,9 +38,10 @@ module ActiveModel # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # - # # Schema: User(name:string, password_digest:string) + # # Schema: User(name:string, password_digest:string, recovery_password_digest:string) # class User < ActiveRecord::Base # has_secure_password + # has_secure_password :recovery_password, validations: false # end # # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch') @@ -48,11 +50,15 @@ module ActiveModel # user.save # => false, confirmation doesn't match # user.password_confirmation = 'mUc3m00RsqyRe' # user.save # => true + # user.recovery_password = "42password" + # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC" + # user.save # => true # user.authenticate('notright') # => false # user.authenticate('mUc3m00RsqyRe') # => user + # user.authenticate_recovery_password('42password') # => user # User.find_by(name: 'david').try(:authenticate, 'notright') # => false # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user - def has_secure_password(options = {}) + def has_secure_password(attribute = :password, validations: true) # Load bcrypt gem only when has_secure_password is used. # This is to avoid ActiveModel (and by extension the entire framework) # being dependent on a binary library. @@ -63,9 +69,40 @@ module ActiveModel raise end - include InstanceMethodsOnActivation + attr_reader attribute + + define_method("#{attribute}=") do |unencrypted_password| + if unencrypted_password.nil? + self.send("#{attribute}_digest=", nil) + elsif !unencrypted_password.empty? + instance_variable_set("@#{attribute}", unencrypted_password) + cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost + self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost)) + end + end + + define_method("#{attribute}_confirmation=") do |unencrypted_password| + instance_variable_set("@#{attribute}_confirmation", unencrypted_password) + end + + # Returns +self+ if the password is correct, otherwise +false+. + # + # class User < ActiveRecord::Base + # has_secure_password validations: false + # end + # + # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') + # user.save + # user.authenticate_password('notright') # => false + # user.authenticate_password('mUc3m00RsqyRe') # => user + define_method("authenticate_#{attribute}") do |unencrypted_password| + attribute_digest = send("#{attribute}_digest") + BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self + end + + alias_method :authenticate, :authenticate_password if attribute == :password - if options.fetch(:validations, true) + if validations include ActiveModel::Validations # This ensures the model has a password by checking whether the password_digest @@ -73,57 +110,13 @@ module ActiveModel # when there is an error, the message is added to the password attribute instead # so that the error message will make sense to the end-user. validate do |record| - record.errors.add(:password, :blank) unless record.password_digest.present? + record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present? end - validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED - validates_confirmation_of :password, allow_blank: true + validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED + validates_confirmation_of attribute, allow_blank: true end end end - - module InstanceMethodsOnActivation - # Returns +self+ if the password is correct, otherwise +false+. - # - # class User < ActiveRecord::Base - # has_secure_password validations: false - # end - # - # user = User.new(name: 'david', password: 'mUc3m00RsqyRe') - # user.save - # user.authenticate('notright') # => false - # user.authenticate('mUc3m00RsqyRe') # => user - def authenticate(unencrypted_password) - BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self - end - - attr_reader :password - - # Encrypts the password into the +password_digest+ attribute, only if the - # new password is not empty. - # - # class User < ActiveRecord::Base - # has_secure_password validations: false - # end - # - # user = User.new - # user.password = nil - # user.password_digest # => nil - # user.password = 'mUc3m00RsqyRe' - # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4." - def password=(unencrypted_password) - if unencrypted_password.nil? - self.password_digest = nil - elsif !unencrypted_password.empty? - @password = unencrypted_password - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost - self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) - end - end - - def password_confirmation=(unencrypted_password) - @password_confirmation = unencrypted_password - end - end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 47cb81bee5..c4b7b32291 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -179,7 +179,7 @@ module ActiveModel return unless includes = options[:include] unless includes.is_a?(Hash) - includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] + includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }] end includes.each do |association, opts| diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 25e1541d66..f77fb98c32 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -26,13 +26,13 @@ module ActiveModel # user = User.find(1) # user.as_json # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true} + # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true} # # ActiveRecord::Base.include_root_in_json = true # # user.as_json # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true } } + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } } # # This behavior can also be achieved by setting the <tt>:root</tt> option # to +true+ as in: @@ -40,7 +40,7 @@ module ActiveModel # user = User.find(1) # user.as_json(root: true) # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true } } + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } } # # Without any +options+, the returned Hash will include all the model's # attributes. @@ -48,7 +48,7 @@ module ActiveModel # user = User.find(1) # user.as_json # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true} + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true} # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit # the attributes included, and work similar to the +attributes+ method. @@ -63,14 +63,14 @@ module ActiveModel # # user.as_json(methods: :permalink) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "permalink" => "1-konata-izumi" } # # To include associations use <tt>:include</tt>: # # user.as_json(include: :posts) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" }, # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] } # @@ -81,7 +81,7 @@ module ActiveModel # only: :body } }, # only: :title } }) # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, - # # "created_at" => "2006/08/01", "awesome" => true, + # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true, # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ], # # "title" => "Welcome to the weblog" }, # # { "comments" => [ { "body" => "Don't think too hard" } ], @@ -93,11 +93,12 @@ module ActiveModel include_root_in_json end + hash = serializable_hash(options).as_json if root root = model_name.element if root == true - { root => serializable_hash(options) } + { root => hash } else - serializable_hash(options) + hash end end diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index dc2eca18be..76203c5a88 100644 --- a/activemodel/lib/active_model/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -40,7 +40,7 @@ module ActiveModel alias_method :to_str, :to_s def hex - @value.unpack("H*")[0] + @value.unpack1("H*") end def ==(other) diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb index bcdbab0343..f6c6efbc87 100644 --- a/activemodel/lib/active_model/type/boolean.rb +++ b/activemodel/lib/active_model/type/boolean.rb @@ -20,6 +20,10 @@ module ActiveModel :boolean end + def serialize(value) # :nodoc: + cast(value) + end + private def cast_value(value) diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb index edc408f541..c5fe926039 100644 --- a/activemodel/lib/active_model/type/date.rb +++ b/activemodel/lib/active_model/type/date.rb @@ -3,32 +3,17 @@ module ActiveModel module Type class Date < Value # :nodoc: + include Helpers::Timezone include Helpers::AcceptsMultiparameterTime.new def type :date end - def serialize(value) - cast(value) - end - def type_cast_for_schema(value) value.to_s(:db).inspect end - def is_utc? - ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC" - end - - def default_timezone - if is_utc? - :utc - else - :local - end - end - private def cast_value(value) @@ -54,7 +39,7 @@ module ActiveModel end def new_date(year, mon, mday) - if year && year != 0 + unless year.nil? || (year == 0 && mon == 0 && mday == 0) ::Date.new(year, mon, mday) rescue nil end end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb index 9641bf45ee..133410e821 100644 --- a/activemodel/lib/active_model/type/date_time.rb +++ b/activemodel/lib/active_model/type/date_time.rb @@ -3,6 +3,7 @@ module ActiveModel module Type class DateTime < Value # :nodoc: + include Helpers::Timezone include Helpers::TimeValue include Helpers::AcceptsMultiparameterTime.new( defaults: { 4 => 0, 5 => 0 } @@ -12,10 +13,6 @@ module ActiveModel :datetime end - def serialize(value) - super(cast(value)) - end - private def cast_value(value) @@ -39,9 +36,9 @@ module ActiveModel end def value_from_multiparameter_assignment(values_hash) - missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } - if missing_parameter - raise ArgumentError, missing_parameter + missing_parameters = (1..3).select { |key| !values_hash.key?(key) } + if missing_parameters.any? + raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}" end super end diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb index 9dbe32e5a6..ea1987df7c 100644 --- a/activemodel/lib/active_model/type/float.rb +++ b/activemodel/lib/active_model/type/float.rb @@ -18,8 +18,6 @@ module ActiveModel end end - alias serialize cast - private def cast_value(value) diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb index 403f0a9e6b..20145d5f0d 100644 --- a/activemodel/lib/active_model/type/helpers.rb +++ b/activemodel/lib/active_model/type/helpers.rb @@ -4,3 +4,4 @@ require "active_model/type/helpers/accepts_multiparameter_time" require "active_model/type/helpers/numeric" require "active_model/type/helpers/mutable" require "active_model/type/helpers/time_value" +require "active_model/type/helpers/timezone" diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb index ad891f841e..e15d7b013f 100644 --- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -5,6 +5,10 @@ module ActiveModel module Helpers # :nodoc: all class AcceptsMultiparameterTime < Module def initialize(defaults: {}) + define_method(:serialize) do |value| + super(cast(value)) + end + define_method(:cast) do |value| if value.is_a?(Hash) value_from_multiparameter_assignment(value) diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index 16e14f9e5f..444847a210 100644 --- a/activemodel/lib/active_model/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -4,6 +4,10 @@ module ActiveModel module Type module Helpers # :nodoc: all module Numeric + def serialize(value) + cast(value) + end + def cast(value) value = \ case value @@ -29,7 +33,7 @@ module ActiveModel # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value.to_s !~ /\A-?\d+\.?\d*\z/ + !/\A[-+]?\d+/.match?(value.to_s) end end end diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 250c4021c6..735b9a75a6 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "active_support/core_ext/string/zones" require "active_support/core_ext/time/zones" module ActiveModel @@ -20,18 +21,6 @@ module ActiveModel value end - def is_utc? - ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC" - end - - def default_timezone - if is_utc? - :utc - else - :local - end - end - def apply_seconds_precision(value) return value unless precision && value.respond_to?(:usec) number_of_insignificant_digits = 6 - precision @@ -69,7 +58,13 @@ module ActiveModel # Doesn't handle time zones. def fast_string_to_time(string) if string =~ ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i + microsec_part = $7 + if microsec_part && microsec_part.start_with?(".") && microsec_part.length == 7 + microsec_part[0] = "" + microsec = microsec_part.to_i + else + microsec = (microsec_part.to_r * 1_000_000).to_i + end new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activemodel/lib/active_model/type/helpers/timezone.rb b/activemodel/lib/active_model/type/helpers/timezone.rb new file mode 100644 index 0000000000..cf87b9715b --- /dev/null +++ b/activemodel/lib/active_model/type/helpers/timezone.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "active_support/core_ext/time/zones" + +module ActiveModel + module Type + module Helpers # :nodoc: all + module Timezone + def is_utc? + ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC" + end + + def default_timezone + is_utc? ? :utc : :local + end + end + end + end +end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index fe396998a3..5878b94171 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -24,28 +24,18 @@ module ActiveModel end def serialize(value) - result = cast(value) + result = super if result ensure_in_range(result) end result 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 :range - private + attr_reader :range def cast_value(value) - case value - when true then 1 - when false then 0 - else - value.to_i rescue nil - end + value.to_i rescue nil end def ensure_in_range(value) diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb index 7272d7b0c5..a19dc0f011 100644 --- a/activemodel/lib/active_model/type/registry.rb +++ b/activemodel/lib/active_model/type/registry.rb @@ -23,13 +23,8 @@ module ActiveModel end 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 :registrations - private + attr_reader :registrations def registration_klass Registration @@ -59,10 +54,7 @@ module ActiveModel type_name == name end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :name, :block end end diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb index 36f13945b1..a9c9bfadb6 100644 --- a/activemodel/lib/active_model/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -16,8 +16,8 @@ module ActiveModel def cast_value(value) case value when ::String then ::String.new(value) - when true then "t".freeze - when false then "f".freeze + when true then "t" + when false then "f" else value.to_s end end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index ad7ba0351a..16d3efb728 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -3,21 +3,28 @@ module ActiveModel module Type class Time < Value # :nodoc: + include Helpers::Timezone include Helpers::TimeValue include Helpers::AcceptsMultiparameterTime.new( - defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } ) def type :time end + def serialize(value) + super || value + end + def user_input_in_time_zone(value) return unless value.present? case value when ::String value = "2000-01-01 #{value}" + time_hash = ::Date._parse(value) + return if time_hash[:hour].nil? when ::Time value = value.change(year: 2000, day: 1, month: 1) end @@ -28,14 +35,10 @@ module ActiveModel private def cast_value(value) - return value unless value.is_a?(::String) + return apply_seconds_precision(value) unless value.is_a?(::String) return if value.empty? - if value.start_with?("2000-01-01") - dummy_time_value = value - else - dummy_time_value = "2000-01-01 #{value}" - end + dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ") fast_string_to_time(dummy_time_value) || begin time_hash = ::Date._parse(dummy_time_value) diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb index a8ea6a2c22..b6914dd63c 100644 --- a/activemodel/lib/active_model/type/value.rb +++ b/activemodel/lib/active_model/type/value.rb @@ -90,6 +90,10 @@ module ActiveModel false end + def force_equality?(_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 cdf11d190f..f18f9a601a 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/array/extract_options" -require "active_support/core_ext/hash/keys" -require "active_support/core_ext/hash/except" module ActiveModel # == Active \Model \Validations @@ -164,14 +162,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 diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index f35e4dec7f..6fd54270f2 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -54,17 +54,13 @@ module ActiveModel def define_on(klass) attr_readers = attributes.reject { |name| klass.attribute_method?(name) } attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") } - klass.send(:attr_reader, *attr_readers) - klass.send(:attr_writer, *attr_writers) + klass.define_attribute_methods + klass.attr_reader(*attr_readers) + klass.attr_writer(*attr_writers) end - # 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 - private + attr_reader :attributes def convert_to_reader_name(method_name) method_name.to_s.chomp("=") diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4d0ab2a2fe..887d31ae2a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -54,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. @@ -93,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 0b9b5ce6a1..bafb8e2106 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -32,7 +32,7 @@ module ActiveModel @delimiter ||= options[:in] || options[:within] end - # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all + # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all # possible values in the range for equality, which is slower but more accurate. # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range # endpoints, which is fast but is only accurate on Numeric, Time, Date, diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 1b5d5b09ab..b549755ba4 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -19,11 +19,11 @@ module ActiveModel private def setup!(klass) - klass.send(:attr_reader, *attributes.map do |attribute| + klass.attr_reader(*attributes.map do |attribute| :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation") end.compact) - klass.send(:attr_writer, *attributes.map do |attribute| + klass.attr_writer(*attributes.map do |attribute| :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=") end.compact) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 3104e7e329..9c12dc14c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -19,7 +19,7 @@ module ActiveModel # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :role, in: %w( admin contributor ) # validates_inclusion_of :age, in: 0..99 # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index d6c80b2c5d..02759b4ccb 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -32,7 +32,7 @@ module ActiveModel value = options[key] unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc) - raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc" + raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc" end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 31750ba78e..51e224d5cd 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "bigdecimal/util" + module ActiveModel module Validations class NumericalityValidator < EachValidator # :nodoc: @@ -9,6 +11,8 @@ module ActiveModel RESERVED_OPTIONS = CHECKS.keys + [:only_integer] + INTEGER_REGEX = /\A[+-]?\d+\z/ + def check_validity! keys = CHECKS.keys - [:odd, :even] options.slice(*keys).each do |option, value| @@ -19,9 +23,20 @@ module ActiveModel end def validate_each(record, attr_name, value) - before_type_cast = :"#{attr_name}_before_type_cast" + came_from_user = :"#{attr_name}_came_from_user?" - raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast) && record.send(before_type_cast) != value + if record.respond_to?(came_from_user) + if record.public_send(came_from_user) + raw_value = record.read_attribute_before_type_cast(attr_name) + elsif record.respond_to?(:read_attribute) + raw_value = record.read_attribute(attr_name) + end + else + before_type_cast = :"#{attr_name}_before_type_cast" + if record.respond_to?(before_type_cast) + raw_value = record.public_send(before_type_cast) + end + end raw_value ||= value if record_attribute_changed_in_place?(record, attr_name) @@ -38,11 +53,7 @@ module ActiveModel return end - if raw_value.is_a?(Numeric) - value = raw_value - else - value = parse_raw_value_as_a_number(raw_value) - end + value = parse_as_number(raw_value) options.slice(*CHECKS.keys).each do |option, option_value| case option @@ -58,6 +69,8 @@ module ActiveModel option_value = record.send(option_value) end + option_value = parse_as_number(option_value) + unless value.send(CHECKS[option], option_value) record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value)) end @@ -68,18 +81,29 @@ module ActiveModel private def is_number?(raw_value) - !parse_raw_value_as_a_number(raw_value).nil? + !parse_as_number(raw_value).nil? rescue ArgumentError, TypeError false end - def parse_raw_value_as_a_number(raw_value) - return raw_value.to_i if is_integer?(raw_value) - Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ + def parse_as_number(raw_value) + if raw_value.is_a?(Float) + raw_value.to_d + elsif raw_value.is_a?(Numeric) + raw_value + elsif is_integer?(raw_value) + raw_value.to_i + elsif !is_hexadecimal_literal?(raw_value) + Kernel.Float(raw_value).to_d + end end def is_integer?(raw_value) - /\A[+-]?\d+\z/ === raw_value.to_s + INTEGER_REGEX.match?(raw_value.to_s) + end + + def is_hexadecimal_literal?(raw_value) + /\A0[xX]/.match?(raw_value.to_s) end def filtered_options(value) diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e28e7e9219..21c4ce0dfe 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -63,7 +63,7 @@ module ActiveModel # and strings in shortcut form. # # validates :email, format: /@/ - # validates :gender, inclusion: %w(male female) + # validates :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your @@ -116,7 +116,7 @@ module ActiveModel key = "#{key.to_s.camelize}Validator" begin - validator = key.include?("::".freeze) ? key.constantize : const_get(key) + validator = key.include?("::") ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index e17c3ca7b3..94d53b8dd1 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -90,7 +90,7 @@ module ActiveModel # class MyValidator < ActiveModel::Validator # def initialize(options={}) # super - # options[:class].send :attr_accessor, :custom_attribute + # options[:class].attr_accessor :custom_attribute # end # end class Validator diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 5ecf0a69c4..30e8419685 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -18,10 +18,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase raise ErrorFromAttributeWriter end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_writer :metadata end @@ -71,6 +68,14 @@ class AttributeAssignmentTest < ActiveModel::TestCase assert_equal "world", model.description end + test "simple assignment alias" do + model = Model.new + + model.attributes = { name: "hello", description: "world" } + assert_equal "hello", model.name + assert_equal "world", model.description + end + test "assign non-existing attribute" do model = Model.new error = assert_raises(ActiveModel::UnknownAttributeError) do @@ -95,9 +100,11 @@ class AttributeAssignmentTest < ActiveModel::TestCase end test "an ArgumentError is raised if a non-hash-like object is passed" do - assert_raises(ArgumentError) do + err = assert_raises(ArgumentError) do Model.new(1) end + + assert_equal("When assigning attributes, you must pass a hash as an argument, Integer passed.", err.message) end test "forbidden attributes cannot be used for mass assignment" do diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index d2837ec894..ebb6cc542d 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -106,14 +106,12 @@ class AttributeMethodsTest < ActiveModel::TestCase end test "#define_attribute_method generates attribute method" do - begin - ModelWithAttributes.define_attribute_method(:foo) + ModelWithAttributes.define_attribute_method(:foo) - assert_respond_to ModelWithAttributes.new, :foo - assert_equal "value of foo", ModelWithAttributes.new.foo - ensure - ModelWithAttributes.undefine_attribute_methods - end + assert_respond_to ModelWithAttributes.new, :foo + assert_equal "value of foo", ModelWithAttributes.new.foo + ensure + ModelWithAttributes.undefine_attribute_methods end test "#define_attribute_method does not generate attribute method if already defined in attribute module" do @@ -140,36 +138,30 @@ class AttributeMethodsTest < ActiveModel::TestCase end test "#define_attribute_method generates attribute method with invalid identifier characters" do - begin - ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') + ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') - assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' - assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b") - ensure - ModelWithWeirdNamesAttributes.undefine_attribute_methods - end + assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b' + assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b") + ensure + ModelWithWeirdNamesAttributes.undefine_attribute_methods end test "#define_attribute_methods works passing multiple arguments" do - begin - ModelWithAttributes.define_attribute_methods(:foo, :baz) + ModelWithAttributes.define_attribute_methods(:foo, :baz) - assert_equal "value of foo", ModelWithAttributes.new.foo - assert_equal "value of baz", ModelWithAttributes.new.baz - ensure - ModelWithAttributes.undefine_attribute_methods - end + assert_equal "value of foo", ModelWithAttributes.new.foo + assert_equal "value of baz", ModelWithAttributes.new.baz + ensure + ModelWithAttributes.undefine_attribute_methods end test "#define_attribute_methods generates attribute methods" do - begin - ModelWithAttributes.define_attribute_methods(:foo) + ModelWithAttributes.define_attribute_methods(:foo) - assert_respond_to ModelWithAttributes.new, :foo - assert_equal "value of foo", ModelWithAttributes.new.foo - ensure - ModelWithAttributes.undefine_attribute_methods - end + assert_respond_to ModelWithAttributes.new, :foo + assert_equal "value of foo", ModelWithAttributes.new.foo + ensure + ModelWithAttributes.undefine_attribute_methods end test "#alias_attribute generates attribute_aliases lookup hash" do @@ -182,45 +174,39 @@ class AttributeMethodsTest < ActiveModel::TestCase end test "#define_attribute_methods generates attribute methods with spaces in their names" do - begin - ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') - assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' - assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') - ensure - ModelWithAttributesWithSpaces.undefine_attribute_methods - end + assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar' + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar') + ensure + ModelWithAttributesWithSpaces.undefine_attribute_methods end test "#alias_attribute works with attributes with spaces in their names" do - begin - ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') - ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') + ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar') + ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar') - assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar - ensure - ModelWithAttributesWithSpaces.undefine_attribute_methods - end + assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar + ensure + ModelWithAttributesWithSpaces.undefine_attribute_methods end test "#alias_attribute works with attributes named as a ruby keyword" do - begin - ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) - ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) - ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) - - assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from - assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to - ensure - ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods - end + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + ensure + ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods end test "#undefine_attribute_methods removes attribute methods" do ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods - assert !ModelWithAttributes.new.respond_to?(:foo) + assert_not_respond_to ModelWithAttributes.new, :foo assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end @@ -255,7 +241,7 @@ class AttributeMethodsTest < ActiveModel::TestCase m = ModelWithAttributes2.new m.attributes = { "private_method" => "<3", "protected_method" => "O_o" } - assert !m.respond_to?(:private_method) + assert_not_respond_to m, :private_method assert m.respond_to?(:private_method, true) c = ClassWithProtected.new diff --git a/activemodel/test/cases/attribute_set_test.rb b/activemodel/test/cases/attribute_set_test.rb index 6e522d6c80..62feb9074e 100644 --- a/activemodel/test/cases/attribute_set_test.rb +++ b/activemodel/test/cases/attribute_set_test.rb @@ -74,8 +74,8 @@ module ActiveModel clone.freeze - assert clone.frozen? - assert_not attributes.frozen? + assert_predicate clone, :frozen? + assert_not_predicate attributes, :frozen? end test "to_hash returns a hash of the type cast values" do @@ -105,8 +105,8 @@ module ActiveModel test "known columns are built with uninitialized attributes" do attributes = attributes_with_uninitialized_key - assert attributes[:foo].initialized? - assert_not attributes[:bar].initialized? + assert_predicate attributes[:foo], :initialized? + assert_not_predicate attributes[:bar], :initialized? end test "uninitialized attributes are not included in the attributes hash" do @@ -169,7 +169,7 @@ module ActiveModel assert attributes.key?(:foo) assert_equal [:foo], attributes.keys - assert attributes[:foo].initialized? + assert_predicate attributes[:foo], :initialized? end class MyType @@ -209,11 +209,6 @@ module ActiveModel 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") @@ -222,6 +217,22 @@ module ActiveModel assert_equal({ foo: "1" }, attributes.to_hash) end + test "marshalling dump/load legacy materialized attribute hash" do + builder = AttributeSet::Builder.new(foo: Type::String.new) + attributes = builder.build_from_database(foo: "1") + + attributes.instance_variable_get(:@attributes).instance_eval do + class << self + def marshal_dump + materialize + end + end + end + + attributes = Marshal.load(Marshal.dump(attributes)) + 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") @@ -253,5 +264,11 @@ module ActiveModel assert_equal attributes, attributes2 assert_not_equal attributes2, attributes3 end + + private + 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 end end diff --git a/activemodel/test/cases/attribute_test.rb b/activemodel/test/cases/attribute_test.rb index 14d86cef97..20c02e689c 100644 --- a/activemodel/test/cases/attribute_test.rb +++ b/activemodel/test/cases/attribute_test.rb @@ -78,7 +78,7 @@ module ActiveModel end test "duping dups the value" do - @type.expect(:deserialize, "type cast".dup, ["a value"]) + @type.expect(:deserialize, +"type cast", ["a value"]) attribute = Attribute.from_database(nil, "a value", @type) value_from_orig = attribute.value @@ -175,33 +175,33 @@ module ActiveModel 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? + assert_not_predicate 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? + assert_predicate 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? + assert_not_predicate 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? + assert_predicate 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? + assert_not_predicate unchanged, :changed? end test "an attribute can not be mutated if it has not been read, @@ -209,15 +209,15 @@ module ActiveModel 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? + assert_not_predicate 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? + assert_predicate attribute, :changed_in_place? + assert_predicate attribute, :changed? end test "an attribute can forget its changes" do @@ -226,7 +226,7 @@ module ActiveModel forgotten = changed.forgetting_assignment assert changed.changed? # sanity check - refute forgotten.changed? + assert_not_predicate forgotten, :changed? end test "with_value_from_user validates the value" do @@ -246,7 +246,7 @@ module ActiveModel end test "with_type preserves mutations" do - attribute = Attribute.from_database(:foo, "".dup, Type::Value.new) + attribute = Attribute.from_database(:foo, +"", Type::Value.new) attribute.value << "1" assert_equal 1, attribute.with_type(Type::Integer.new).value diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb index 83a86371e0..f9693a23cd 100644 --- a/activemodel/test/cases/attributes_dirty_test.rb +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -25,11 +25,11 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "setting attribute will result in change" do - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? @model.name = "Ringo" - assert @model.changed? - assert @model.name_changed? + assert_predicate @model, :changed? + assert_predicate @model, :name_changed? end test "list of changed attribute keys" do @@ -39,7 +39,7 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end @@ -71,35 +71,35 @@ class AttributesDirtyTest < ActiveModel::TestCase test "attribute mutation" do @model.name = "Yam" @model.save - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? @model.name.replace("Hadad") - assert @model.name_changed? + assert_predicate @model, :name_changed? end test "resetting attribute" do @model.name = "Bob" @model.restore_name! assert_nil @model.name - assert !@model.name_changed? + assert_not_predicate @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? + assert_predicate @model, :color_changed? @model.save - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? @model.color = "red" - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? end test "saving should reset model's changed status" do @model.name = "Alf" - assert @model.changed? + assert_predicate @model, :changed? @model.save - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? end test "saving should preserve previous changes" do @@ -118,7 +118,7 @@ class AttributesDirtyTest < ActiveModel::TestCase test "saving should preserve model's previous changed status" do @model.name = "Jericho Cane" @model.save - assert @model.name_previously_changed? + assert_predicate @model, :name_previously_changed? end test "previous value is preserved when changed after save" do @@ -143,7 +143,7 @@ class AttributesDirtyTest < ActiveModel::TestCase test "using attribute_will_change! with a symbol" do @model.size = 1 - assert @model.size_changed? + assert_predicate @model, :size_changed? end test "reload should reset all changes" do @@ -170,7 +170,7 @@ class AttributesDirtyTest < ActiveModel::TestCase @model.restore_attributes - assert_not @model.changed? + assert_not_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "Red", @model.color end @@ -184,7 +184,7 @@ class AttributesDirtyTest < ActiveModel::TestCase @model.restore_attributes(["name"]) - assert @model.changed? + assert_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "White", @model.color end diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb index e43bf15335..5483fb101d 100644 --- a/activemodel/test/cases/attributes_test.rb +++ b/activemodel/test/cases/attributes_test.rb @@ -47,6 +47,26 @@ module ActiveModel assert_equal true, data.boolean_field end + test "reading attributes" do + data = ModelForAttributesTest.new( + integer_field: 1.1, + string_field: 1.1, + decimal_field: 1.1, + boolean_field: 1.1 + ) + + expected_attributes = { + integer_field: 1, + string_field: "1.1", + decimal_field: BigDecimal("1.1"), + string_with_default: "default string", + date_field: Date.new(2016, 1, 1), + boolean_field: true + }.stringify_keys + + assert_equal expected_attributes, data.attributes + end + test "nonexistent attribute" do assert_raise ActiveModel::UnknownAttributeError do ModelForAttributesTest.new(nonexistent: "nonexistent") @@ -64,5 +84,14 @@ module ActiveModel assert_equal "4.4", data.integer_field end + + test "attributes with proc defaults can be marshalled" do + data = ModelForAttributesTest.new + attributes = data.instance_variable_get(:@attributes) + round_tripped = Marshal.load(Marshal.dump(data)) + new_attributes = round_tripped.instance_variable_get(:@attributes) + + assert_equal attributes, new_attributes + end end end diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index a5d29d0f22..0711dc56ca 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -85,21 +85,21 @@ class CallbacksTest < ActiveModel::TestCase end test "only selects which types of callbacks should be created" do - assert !ModelCallbacks.respond_to?(:before_initialize) - assert !ModelCallbacks.respond_to?(:around_initialize) + assert_not_respond_to ModelCallbacks, :before_initialize + assert_not_respond_to ModelCallbacks, :around_initialize assert_respond_to ModelCallbacks, :after_initialize end test "only selects which types of callbacks should be created from an array list" do assert_respond_to ModelCallbacks, :before_multiple assert_respond_to ModelCallbacks, :around_multiple - assert !ModelCallbacks.respond_to?(:after_multiple) + assert_not_respond_to ModelCallbacks, :after_multiple end test "no callbacks should be created" do - assert !ModelCallbacks.respond_to?(:before_empty) - assert !ModelCallbacks.respond_to?(:around_empty) - assert !ModelCallbacks.respond_to?(:after_empty) + assert_not_respond_to ModelCallbacks, :before_empty + assert_not_respond_to ModelCallbacks, :around_empty + assert_not_respond_to ModelCallbacks, :after_empty end class Violin @@ -112,7 +112,7 @@ class CallbacksTest < ActiveModel::TestCase def callback1; history << "callback1"; end def callback2; history << "callback2"; end def create - run_callbacks(:create) {} + run_callbacks(:create) { } self end end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index dfe041ff50..0edbbffa86 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -5,41 +5,37 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods :name, :color, :size + define_attribute_methods :name, :color, :size, :status def initialize @name = nil @color = nil @size = nil + @status = "initialized" end - def name - @name - end + attr_reader :name, :color, :size, :status def name=(val) name_will_change! @name = val end - def color - @color - end - def color=(val) color_will_change! unless val == @color @color = val end - def size - @size - end - def size=(val) attribute_will_change!(:size) unless val == @size @size = val end + def status=(val) + status_will_change! unless val == @status + @status = val + end + def save changes_applied end @@ -54,11 +50,11 @@ class DirtyTest < ActiveModel::TestCase end test "setting attribute will result in change" do - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? @model.name = "Ringo" - assert @model.changed? - assert @model.name_changed? + assert_predicate @model, :changed? + assert_predicate @model, :name_changed? end test "list of changed attribute keys" do @@ -68,7 +64,7 @@ class DirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end @@ -98,83 +94,94 @@ class DirtyTest < ActiveModel::TestCase end test "attribute mutation" do - @model.instance_variable_set("@name", "Yam".dup) - assert !@model.name_changed? + @model.instance_variable_set("@name", +"Yam") + assert_not_predicate @model, :name_changed? @model.name.replace("Hadad") - assert !@model.name_changed? + assert_not_predicate @model, :name_changed? @model.name_will_change! @model.name.replace("Baal") - assert @model.name_changed? + assert_predicate @model, :name_changed? end test "resetting attribute" do @model.name = "Bob" @model.restore_name! assert_nil @model.name - assert !@model.name_changed? + assert_not_predicate @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? + assert_predicate @model, :color_changed? @model.save - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? @model.color = "red" - assert !@model.color_changed? - assert !@model.changed? + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? end test "saving should reset model's changed status" do @model.name = "Alf" - assert @model.changed? + assert_predicate @model, :changed? @model.save - assert !@model.changed? - assert !@model.name_changed? + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? end test "saving should preserve previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"] + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "setting new attributes should not affect previous changes" do @model.name = "Jericho Cane" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" + @model.status = "finished" assert_equal [nil, "Jericho Cane"], @model.name_previous_change + assert_equal ["initialized", "waiting"], @model.previous_changes["status"] end test "saving should preserve model's previous changed status" do @model.name = "Jericho Cane" @model.save - assert @model.name_previously_changed? + assert_predicate @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.status = "waiting" + assert_equal({ "name" => nil, "status" => "initialized" }, @model.changed_attributes) @model.save @model.name = "John" - assert_equal({ "name" => "Paul" }, @model.changed_attributes) + @model.status = "finished" + assert_equal({ "name" => "Paul", "status" => "waiting" }, @model.changed_attributes) end test "changing the same attribute multiple times retains the correct original value" do @model.name = "Otto" + @model.status = "waiting" @model.save @model.name = "DudeFella ManGuy" @model.name = "Mr. Manfredgensonton" + @model.status = "processing" + @model.status = "finished" assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal ["waiting", "finished"], @model.status_change assert_equal @model.name_was, "Otto" end test "using attribute_will_change! with a symbol" do @model.size = 1 - assert @model.size_changed? + assert_predicate @model, :size_changed? end test "reload should reset all changes" do @@ -201,7 +208,7 @@ class DirtyTest < ActiveModel::TestCase @model.restore_attributes - assert_not @model.changed? + assert_not_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "Red", @model.color end @@ -215,7 +222,7 @@ class DirtyTest < ActiveModel::TestCase @model.restore_attributes(["name"]) - assert @model.changed? + assert_predicate @model, :changed? assert_equal "Dmitry", @model.name assert_equal "White", @model.color end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index d5c282b620..947f9bf99b 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "cases/helper" -require "active_support/core_ext/string/strip" require "yaml" class ErrorsTest < ActiveModel::TestCase @@ -83,7 +82,7 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.count person.errors.clear - assert person.errors.empty? + assert_empty person.errors end test "error access is indifferent" do @@ -128,8 +127,8 @@ class ErrorsTest < ActiveModel::TestCase test "detecting whether there are errors with empty?, blank?, include?" do person = Person.new person.errors[:foo] - assert person.errors.empty? - assert person.errors.blank? + assert_empty person.errors + assert_predicate person.errors, :blank? assert_not_includes person.errors, :foo end @@ -186,6 +185,12 @@ class ErrorsTest < ActiveModel::TestCase assert person.errors.added?(:name, :blank) end + test "added? returns true when string attribute is used with a symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.added?("name", :blank) + end + test "added? handles proc messages" do person = Person.new message = Proc.new { "cannot be blank" } @@ -204,30 +209,118 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "cannot be blank") person.errors.add(:name, "is invalid") assert person.errors.added?(:name, "cannot be blank") + assert person.errors.added?(:name, "is invalid") + assert_not person.errors.added?(:name, "incorrect") end test "added? returns false when no errors are present" do person = Person.new - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do person = Person.new person.errors.add(:name, "is invalid") - assert !person.errors.added?(:name, "cannot be blank") + assert_not person.errors.added?(:name, "cannot be blank") end - test "added? returns false when checking for an error, but not providing message arguments" do + test "added? returns false when checking for an error, but not providing message argument" do person = Person.new person.errors.add(:name, "cannot be blank") - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) + end + + test "added? returns false when checking for an error with an incorrect or missing option" do + person = Person.new + person.errors.add :name, :too_long, count: 25 + + assert person.errors.added? :name, :too_long, count: 25 + assert person.errors.added? :name, "is too long (maximum is 25 characters)" + assert_not person.errors.added? :name, :too_long, count: 24 + assert_not person.errors.added? :name, :too_long + assert_not person.errors.added? :name, "is too long" end test "added? returns false when checking for an error by symbol and a different error with same message is present" do I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) person = Person.new person.errors.add(:name, :wrong) - assert !person.errors.added?(:name, :used) + assert_not person.errors.added?(:name, :used) + assert person.errors.added?(:name, :wrong) + end + + test "of_kind? returns false when checking for an error, but not providing message argument" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_not person.errors.of_kind?(:name) + end + + test "of_kind? returns false when checking a nonexisting error and other errors are present for the given attribute" do + person = Person.new + person.errors.add(:name, "is invalid") + assert_not person.errors.of_kind?(:name, "cannot be blank") + end + + test "of_kind? returns false when no errors are present" do + person = Person.new + assert_not person.errors.of_kind?(:name) + end + + test "of_kind? matches the given message when several errors are present for the same attribute" do + person = Person.new + person.errors.add(:name, "cannot be blank") + person.errors.add(:name, "is invalid") + assert person.errors.of_kind?(:name, "cannot be blank") + assert person.errors.of_kind?(:name, "is invalid") + assert_not person.errors.of_kind?(:name, "incorrect") + end + + test "of_kind? defaults message to :invalid" do + person = Person.new + person.errors.add(:name) + assert person.errors.of_kind?(:name) + end + + test "of_kind? handles proc messages" do + person = Person.new + message = Proc.new { "cannot be blank" } + person.errors.add(:name, message) + assert person.errors.of_kind?(:name, message) + end + + test "of_kind? returns true when string attribute is used with a symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.of_kind?("name", :blank) + end + + test "of_kind? handles symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.of_kind?(:name, :blank) + end + + test "of_kind? detects indifferent if a specific error was added to the object" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert person.errors.of_kind?(:name, "cannot be blank") + assert person.errors.of_kind?("name", "cannot be blank") + end + + test "of_kind? ignores options" do + person = Person.new + person.errors.add :name, :too_long, count: 25 + + assert person.errors.of_kind? :name, :too_long + assert person.errors.of_kind? :name, "is too long (maximum is 25 characters)" + end + + test "of_kind? returns false when checking for an error by symbol and a different error with same message is present" do + I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) + person = Person.new + person.errors.add(:name, :wrong) + assert_not person.errors.of_kind?(:name, :used) + assert person.errors.of_kind?(:name, :wrong) end test "size calculates the number of error messages" do @@ -320,7 +413,7 @@ class ErrorsTest < ActiveModel::TestCase test "generate_message works without i18n_scope" do person = Person.new - assert !Person.respond_to?(:i18n_scope) + assert_not_respond_to Person, :i18n_scope assert_nothing_raised { person.errors.generate_message(:name, :blank) } @@ -371,7 +464,7 @@ class ErrorsTest < ActiveModel::TestCase assert_equal 1, person.errors.details.count person.errors.clear - assert person.errors.details.empty? + assert_empty person.errors.details end test "copy errors" do @@ -396,6 +489,30 @@ class ErrorsTest < ActiveModel::TestCase assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details) end + test "slice! removes all errors except the given keys" do + person = Person.new + person.errors.add(:name, "cannot be nil") + person.errors.add(:age, "cannot be nil") + person.errors.add(:gender, "cannot be nil") + person.errors.add(:city, "cannot be nil") + + person.errors.slice!(:age, "gender") + + assert_equal [:age, :gender], person.errors.keys + end + + test "slice! returns the deleted errors" do + person = Person.new + person.errors.add(:name, "cannot be nil") + person.errors.add(:age, "cannot be nil") + person.errors.add(:gender, "cannot be nil") + person.errors.add(:city, "cannot be nil") + + removed_errors = person.errors.slice!(:age, "gender") + + assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors) + end + test "errors are marshalable" do errors = ActiveModel::Errors.new(Person.new) errors.add(:name, :invalid) @@ -406,7 +523,7 @@ class ErrorsTest < ActiveModel::TestCase end test "errors are backward compatible with the Rails 4.2 format" do - yaml = <<-CODE.strip_heredoc + yaml = <<~CODE --- !ruby/object:ActiveModel::Errors base: &1 !ruby/object:ErrorsTest::Person errors: !ruby/object:ActiveModel::Errors diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 91fb9d0a7c..138b1d1bb9 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -14,12 +14,14 @@ require "active_support/testing/method_call_assertions" class ActiveModel::TestCase < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions - # Skips the current run on Rubinius using Minitest::Assertions#skip - private def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" - end - # Skips the current run on JRuby using Minitest::Assertions#skip - private def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) - end + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 009f1f47af..4693da434c 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -248,7 +248,7 @@ class NamingHelpersTest < ActiveModel::TestCase def test_uncountable assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable" - assert !uncountable?(@klass), "Expected 'contact' to be countable" + assert_not uncountable?(@klass), "Expected 'contact' to be countable" end def test_uncountable_route_key diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb index ff5022e960..ab60285e2a 100644 --- a/activemodel/test/cases/railtie_test.rb +++ b/activemodel/test/cases/railtie_test.rb @@ -31,4 +31,24 @@ class RailtieTest < ActiveModel::TestCase assert_equal true, ActiveModel::SecurePassword.min_cost end + + test "i18n full message defaults to false" do + @app.initialize! + + assert_equal false, ActiveModel::Errors.i18n_full_message + end + + test "i18n full message can be disabled" do + @app.config.active_model.i18n_full_message = false + @app.initialize! + + assert_equal false, ActiveModel::Errors.i18n_full_message + end + + test "i18n full message can be enabled" do + @app.config.active_model.i18n_full_message = true + @app.initialize! + + assert_equal true, ActiveModel::Errors.i18n_full_message + end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index d19e81a119..bbf443290b 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -49,14 +49,14 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and a blank password" do @user.password = "" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["can't be blank"], @user.errors[:password] end test "create a new user with validation and a nil password" do @user.password = nil - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["can't be blank"], @user.errors[:password] end @@ -64,7 +64,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and password length greater than 72" do @user.password = "a" * 73 @user.password_confirmation = "a" * 73 - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password] end @@ -72,7 +72,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and a blank password confirmation" do @user.password = "password" @user.password_confirmation = "" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end @@ -86,7 +86,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "create a new user with validation and an incorrect password confirmation" do @user.password = "password" @user.password_confirmation = "something else" - assert !@user.valid?(:create), "user should be invalid" + assert_not @user.valid?(:create), "user should be invalid" assert_equal 1, @user.errors.count assert_equal ["doesn't match Password"], @user.errors[:password_confirmation] end @@ -125,7 +125,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and a nil password" do @existing_user.password = nil - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end @@ -133,7 +133,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and password length greater than 72" do @existing_user.password = "a" * 73 @existing_user.password_confirmation = "a" * 73 - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password] end @@ -141,7 +141,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and a blank password confirmation" do @existing_user.password = "password" @existing_user.password_confirmation = "" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end @@ -155,21 +155,21 @@ class SecurePasswordTest < ActiveModel::TestCase test "updating an existing user with validation and an incorrect password confirmation" do @existing_user.password = "password" @existing_user.password_confirmation = "something else" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation] end test "updating an existing user with validation and a blank password digest" do @existing_user.password_digest = "" - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end test "updating an existing user with validation and a nil password digest" do @existing_user.password_digest = nil - assert !@existing_user.valid?(:update), "user should be invalid" + assert_not @existing_user.valid?(:update), "user should be invalid" assert_equal 1, @existing_user.errors.count assert_equal ["can't be blank"], @existing_user.errors[:password] end @@ -186,9 +186,16 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" + @user.recovery_password = "42password" - assert !@user.authenticate("wrong") - assert @user.authenticate("secret") + assert_equal false, @user.authenticate("wrong") + assert_equal @user, @user.authenticate("secret") + + assert_equal false, @user.authenticate_password("wrong") + assert_equal @user, @user.authenticate_password("secret") + + assert_equal false, @user.authenticate_recovery_password("wrong") + assert_equal @user, @user.authenticate_recovery_password("42password") end test "Password digest cost defaults to bcrypt default cost when min_cost is false" do @@ -199,16 +206,14 @@ class SecurePasswordTest < ActiveModel::TestCase end test "Password digest cost honors bcrypt cost attribute when min_cost is false" do - begin - original_bcrypt_cost = BCrypt::Engine.cost - ActiveModel::SecurePassword.min_cost = false - BCrypt::Engine.cost = 5 - - @user.password = "secret" - assert_equal BCrypt::Engine.cost, @user.password_digest.cost - ensure - BCrypt::Engine.cost = original_bcrypt_cost - end + original_bcrypt_cost = BCrypt::Engine.cost + ActiveModel::SecurePassword.min_cost = false + BCrypt::Engine.cost = 5 + + @user.password = "secret" + assert_equal BCrypt::Engine.cost, @user.password_digest.cost + ensure + BCrypt::Engine.cost = original_bcrypt_cost end test "Password digest cost can be set to bcrypt min cost to speed up tests" do diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 9002982e7f..6826d2bbd1 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -174,4 +174,11 @@ class SerializationTest < ActiveModel::TestCase { "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] } assert_equal expected, @user.serializable_hash(include: [{ address: { only: "street" } }, :friends]) end + + def test_all_includes_with_options + expected = { "email" => "david@example.com", "gender" => "male", "name" => "David", + "address" => { "street" => "123 Lane" }, + "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] } + assert_equal expected, @user.serializable_hash(include: [address: { only: "street" }, friends: { only: "name" }]) + end end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index aae98c9fe4..84efc8de0d 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -26,20 +26,18 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should include root in json if include_root_in_json is true" do - begin - original_include_root_in_json = Contact.include_root_in_json - Contact.include_root_in_json = true - json = @contact.to_json - - assert_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = original_include_root_in_json - end + original_include_root_in_json = Contact.include_root_in_json + Contact.include_root_in_json = true + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json + ensure + Contact.include_root_in_json = original_include_root_in_json end test "should include root in json (option) even if the default is set to false" do @@ -129,20 +127,22 @@ class JsonSerializationTest < ActiveModel::TestCase assert_equal :name, options[:except] end + test "as_json should serialize timestamps" do + assert_equal "2006-08-01T00:00:00.000Z", @contact.as_json["created_at"] + end + test "as_json should return a hash if include_root_in_json is true" do - begin - original_include_root_in_json = Contact.include_root_in_json - Contact.include_root_in_json = true - json = @contact.as_json - - assert_kind_of Hash, json - assert_kind_of Hash, json["contact"] - %w(name age created_at awesome preferences).each do |field| - assert_equal @contact.send(field), json["contact"][field] - end - ensure - Contact.include_root_in_json = original_include_root_in_json + original_include_root_in_json = Contact.include_root_in_json + Contact.include_root_in_json = true + json = @contact.as_json + + assert_kind_of Hash, json + assert_kind_of Hash, json["contact"] + %w(name age created_at awesome preferences).each do |field| + assert_equal @contact.send(field).as_json, json["contact"][field] end + ensure + Contact.include_root_in_json = original_include_root_in_json end test "from_json should work without a root (class attribute)" do diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb index 2d33579595..2de0f53640 100644 --- a/activemodel/test/cases/type/boolean_test.rb +++ b/activemodel/test/cases/type/boolean_test.rb @@ -7,8 +7,8 @@ module ActiveModel class BooleanTest < ActiveModel::TestCase def test_type_cast_boolean type = Type::Boolean.new - assert type.cast("").nil? - assert type.cast(nil).nil? + assert_predicate type.cast(""), :nil? + assert_predicate type.cast(nil), :nil? assert type.cast(true) assert type.cast(1) diff --git a/activemodel/test/cases/type/date_test.rb b/activemodel/test/cases/type/date_test.rb index c50a549d3d..2dd1a55616 100644 --- a/activemodel/test/cases/type/date_test.rb +++ b/activemodel/test/cases/type/date_test.rb @@ -12,8 +12,11 @@ module ActiveModel assert_nil type.cast(" ") assert_nil type.cast("ABC") - date_string = ::Time.now.utc.strftime("%F") + now = ::Time.now.utc + values_hash = { 1 => now.year, 2 => now.mon, 3 => now.mday } + date_string = now.strftime("%F") assert_equal date_string, type.cast(date_string).strftime("%F") + assert_equal date_string, type.cast(values_hash).strftime("%F") end def test_returns_correct_year diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb index 60f62becc2..74b47d1b4d 100644 --- a/activemodel/test/cases/type/date_time_test.rb +++ b/activemodel/test/cases/type/date_time_test.rb @@ -25,6 +25,17 @@ module ActiveModel end end + def test_hash_to_time + type = Type::DateTime.new + assert_equal ::Time.utc(2018, 10, 15, 0, 0, 0), type.cast(1 => 2018, 2 => 10, 3 => 15) + end + + def test_hash_with_wrong_keys + type = Type::DateTime.new + error = assert_raises(ArgumentError) { type.cast(a: 1) } + assert_equal "Provided hash {:a=>1} doesn't contain necessary keys: [1, 2, 3]", error.message + end + private def with_timezone_config(default:) diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index c0cf6ce590..be60c4f7fa 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -57,9 +57,12 @@ module ActiveModel def test_changed? type = Decimal.new - assert type.changed?(5.0, 5.0, "5.0wibble") + assert type.changed?(0.0, 0, "wibble") + assert type.changed?(5.0, 0, "wibble") + assert_not type.changed?(5.0, 5.0, "5.0wibble") assert_not type.changed?(5.0, 5.0, "5.0") assert_not type.changed?(-5.0, -5.0, "-5.0") + assert_not type.changed?(5.0, 5.0, "0.5e+1") end def test_scale_is_applied_before_precision_to_prevent_rounding_errors diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb index 28318e06f8..230a8dda32 100644 --- a/activemodel/test/cases/type/float_test.rb +++ b/activemodel/test/cases/type/float_test.rb @@ -21,9 +21,12 @@ module ActiveModel def test_changing_float type = Type::Float.new - assert type.changed?(5.0, 5.0, "5wibble") + assert type.changed?(0.0, 0, "wibble") + assert type.changed?(5.0, 0, "wibble") + assert_not type.changed?(5.0, 5.0, "5wibble") assert_not type.changed?(5.0, 5.0, "5") assert_not type.changed?(5.0, 5.0, "5.0") + assert_not type.changed?(500.0, 500.0, "0.5E+4") assert_not type.changed?(nil, nil, nil) end end diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index 8c5d18c9b3..9bd0110099 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -50,12 +50,23 @@ module ActiveModel assert_equal 7200, type.cast(2.hours) end + test "casting empty string" do + type = Type::Integer.new + assert_nil type.cast("") + assert_nil type.serialize("") + assert_equal 0, type.deserialize("") + end + test "changed?" do type = Type::Integer.new - assert type.changed?(5, 5, "5wibble") + assert type.changed?(0, 0, "wibble") + assert type.changed?(5, 0, "wibble") + assert_not type.changed?(5, 5, "5wibble") assert_not type.changed?(5, 5, "5") assert_not type.changed?(5, 5, "5.0") + assert_not type.changed?(5, 5, "+5") + assert_not type.changed?(5, 5, "+5.0") assert_not type.changed?(-5, -5, "-5") assert_not type.changed?(-5, -5, "-5.0") assert_not type.changed?(nil, nil, nil) diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb index 825c8bb246..9cc530e8db 100644 --- a/activemodel/test/cases/type/string_test.rb +++ b/activemodel/test/cases/type/string_test.rb @@ -12,14 +12,22 @@ module ActiveModel assert_equal "123", type.cast(123) end + test "type casting for database" do + type = Type::String.new + object, array, hash = Object.new, [true], { a: :b } + assert_equal object, type.serialize(object) + assert_equal array, type.serialize(array) + assert_equal hash, type.serialize(hash) + end + test "cast strings are mutable" do type = Type::String.new - s = "foo".dup + s = +"foo" assert_equal false, type.cast(s).frozen? assert_equal false, s.frozen? - f = "foo".freeze + f = -"foo" assert_equal false, type.cast(f).frozen? assert_equal true, f.frozen? end diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb index f7102d1e97..5c6271241d 100644 --- a/activemodel/test/cases/type/time_test.rb +++ b/activemodel/test/cases/type/time_test.rb @@ -16,6 +16,22 @@ module ActiveModel assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00") assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00") + assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast(4 => 16, 5 => 45, 6 => 54) + end + + def test_user_input_in_time_zone + ::Time.use_zone("Pacific Time (US & Canada)") do + type = Type::Time.new + assert_nil type.user_input_in_time_zone(nil) + assert_nil type.user_input_in_time_zone("") + assert_nil type.user_input_in_time_zone("ABC") + + offset = ::Time.zone.formatted_offset + time_string = "2015-02-09T19:45:54#{offset}" + + assert_equal 19, type.user_input_in_time_zone(time_string).hour + assert_equal offset, type.user_input_in_time_zone(time_string).formatted_offset + end end end end diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index 801577474a..8bc4f4723a 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -17,16 +17,16 @@ class AbsenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "foo" t.content = "bar" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:title] assert_equal ["must be blank"], t.errors[:content] t.title = "" t.content = "something" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:content] assert_equal [], t.errors[:title] t.content = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_absence_of_with_array_arguments @@ -34,7 +34,7 @@ class AbsenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "foo" t.content = "bar" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be blank"], t.errors[:title] assert_equal ["must be blank"], t.errors[:content] end @@ -43,7 +43,7 @@ class AbsenceValidationTest < ActiveModel::TestCase Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes" p = Person.new p.karma = "good" - assert p.invalid? + assert_predicate p, :invalid? assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last end @@ -51,19 +51,19 @@ class AbsenceValidationTest < ActiveModel::TestCase Person.validates_absence_of :karma p = Person.new p.karma = "good" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be blank"], p.errors[:karma] p.karma = nil - assert p.valid? + assert_predicate p, :valid? end def test_validates_absence_of_for_ruby_class_with_custom_reader CustomReader.validates_absence_of :karma p = CustomReader.new p[:karma] = "excellent" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be blank"], p.errors[:karma] p[:karma] = "" - assert p.valid? + assert_predicate p, :valid? end end diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb index c5f54b1868..7662f996ae 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -15,54 +15,54 @@ class AcceptanceValidationTest < ActiveModel::TestCase Topic.validates_acceptance_of(:terms_of_service) t = Topic.new("title" => "We should not be confirmed") - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement Topic.validates_acceptance_of(:terms_of_service) t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = "1" - assert t.valid? + assert_predicate t, :valid? end def test_eula Topic.validates_acceptance_of(:eula, message: "must be abided") t = Topic.new("title" => "We should be confirmed", "eula" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be abided"], t.errors[:eula] t.eula = "1" - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement_with_accept_value Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.") t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = "I agree." - assert t.valid? + assert_predicate t, :valid? end def test_terms_of_service_agreement_with_multiple_accept_values Topic.validates_acceptance_of(:terms_of_service, accept: [1, "I concur."]) t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["must be accepted"], t.errors[:terms_of_service] t.terms_of_service = 1 - assert t.valid? + assert_predicate t, :valid? t.terms_of_service = "I concur." - assert t.valid? + assert_predicate t, :valid? end def test_validates_acceptance_of_for_ruby_class @@ -71,11 +71,11 @@ class AcceptanceValidationTest < ActiveModel::TestCase p = Person.new p.karma = "" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["must be accepted"], p.errors[:karma] p.karma = "1" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -83,6 +83,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase def test_validates_acceptance_of_true Topic.validates_acceptance_of(:terms_of_service) - assert Topic.new(terms_of_service: true).valid? + assert_predicate Topic.new(terms_of_service: true), :valid? end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index d3a9a17a05..ff3cf61746 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -59,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 } @@ -98,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 caea8b65ef..1704db9a48 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -13,24 +13,24 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns true Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? 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_predicate t, :invalid? + assert_predicate 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_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -38,21 +38,21 @@ class ConditionalValidationTest < ActiveModel::TestCase # When the method returns true Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end 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_predicate t, :valid? assert_empty t.errors[:title] 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_predicate t, :valid? assert_empty t.errors[:title] end @@ -60,7 +60,7 @@ class ConditionalValidationTest < ActiveModel::TestCase # 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_predicate t, :valid? assert_empty t.errors[:title] end @@ -68,8 +68,8 @@ class ConditionalValidationTest < ActiveModel::TestCase # 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? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -78,8 +78,8 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: Proc.new { |r| r.content.size > 4 }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -88,7 +88,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: Proc.new { |r| r.content.size > 4 }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -97,7 +97,7 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: Proc.new { |r| r.title != "uhohuhoh" }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end @@ -106,23 +106,23 @@ class ConditionalValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: Proc.new { |r| r.title != "uhohuhoh" }) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end 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_predicate t, :valid? assert_empty t.errors[:title] end 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_predicate t, :invalid? + assert_predicate 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 8b2c65289b..7bf15e4bee 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -14,40 +14,40 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title) t = Topic.new(author_name: "Plutarch") - assert t.valid? + assert_predicate t, :valid? t.title_confirmation = "Parallel Lives" - assert t.invalid? + assert_predicate t, :invalid? t.title_confirmation = nil t.title = "Parallel Lives" - assert t.valid? + assert_predicate t, :valid? t.title_confirmation = "Parallel Lives" - assert t.valid? + assert_predicate t, :valid? end def test_title_confirmation Topic.validates_confirmation_of(:title) t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") - assert t.invalid? + assert_predicate t, :invalid? t.title_confirmation = "We should be confirmed" - assert t.valid? + assert_predicate 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? + assert_predicate t, :valid? t.approved_confirmation = false - assert t.invalid? + assert_predicate t, :invalid? t.approved_confirmation = true - assert t.valid? + assert_predicate t, :valid? end def test_validates_confirmation_of_for_ruby_class @@ -55,35 +55,33 @@ class ConfirmationValidationTest < ActiveModel::TestCase p = Person.new p.karma_confirmation = "None" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["doesn't match Karma"], p.errors[:karma_confirmation] p.karma = "None" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end def test_title_confirmation_with_i18n_attribute - begin - @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend - I18n.load_path.clear - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations("en", - errors: { messages: { confirmation: "doesn't match %{attribute}" } }, - activemodel: { attributes: { topic: { title: "Test Title" } } }) - - Topic.validates_confirmation_of(:title) - - t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") - assert t.invalid? - assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] - ensure - I18n.load_path.replace @old_load_path - I18n.backend = @old_backend - I18n.backend.reload! - end + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations("en", + errors: { messages: { confirmation: "doesn't match %{attribute}" } }, + activemodel: { attributes: { topic: { title: "Test Title" } } }) + + Topic.validates_confirmation_of(:title) + + t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") + assert_predicate t, :invalid? + assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] + ensure + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + I18n.backend.reload! end test "does not override confirmation reader if present" do @@ -122,13 +120,13 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title, case_sensitive: true) t = Topic.new(title: "title", title_confirmation: "Title") - assert t.invalid? + assert_predicate t, :invalid? end def test_title_confirmation_with_case_sensitive_option_false Topic.validates_confirmation_of(:title, case_sensitive: false) t = Topic.new(title: "title", title_confirmation: "Title") - assert t.valid? + assert_predicate t, :valid? end end diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index 68d611e904..50bd47065c 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -14,8 +14,8 @@ class ExclusionValidationTest < ActiveModel::TestCase def test_validates_exclusion_of Topic.validates_exclusion_of(:title, in: %w( abe monkey )) - assert Topic.new("title" => "something", "content" => "abc").valid? - assert Topic.new("title" => "monkey", "content" => "abc").invalid? + assert_predicate Topic.new("title" => "something", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "monkey", "content" => "abc"), :invalid? end def test_validates_exclusion_of_with_formatted_message @@ -24,8 +24,8 @@ class ExclusionValidationTest < ActiveModel::TestCase assert Topic.new("title" => "something", "content" => "abc") t = Topic.new("title" => "monkey") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["option monkey is restricted"], t.errors[:title] end @@ -35,8 +35,8 @@ class ExclusionValidationTest < ActiveModel::TestCase assert Topic.new("title" => "something", "content" => "abc") t = Topic.new("title" => "monkey") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? end def test_validates_exclusion_of_for_ruby_class @@ -44,12 +44,12 @@ class ExclusionValidationTest < ActiveModel::TestCase p = Person.new p.karma = "abe" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is reserved"], p.errors[:karma] p.karma = "Lifo" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -60,26 +60,26 @@ class ExclusionValidationTest < ActiveModel::TestCase t = Topic.new t.title = "elephant" t.author_name = "sikachu" - assert t.invalid? + assert_predicate t, :invalid? t.title = "wasabi" - assert t.valid? + assert_predicate t, :valid? end def test_validates_exclusion_of_with_range Topic.validates_exclusion_of :content, in: ("a".."g") - assert Topic.new(content: "g").invalid? - assert Topic.new(content: "h").valid? + assert_predicate Topic.new(content: "g"), :invalid? + assert_predicate Topic.new(content: "h"), :valid? end def test_validates_exclusion_of_with_time_range Topic.validates_exclusion_of :created_at, in: 6.days.ago..2.days.ago - assert Topic.new(created_at: 5.days.ago).invalid? - assert Topic.new(created_at: 3.days.ago).invalid? - assert Topic.new(created_at: 7.days.ago).valid? - assert Topic.new(created_at: 1.day.ago).valid? + assert_predicate Topic.new(created_at: 5.days.ago), :invalid? + assert_predicate Topic.new(created_at: 3.days.ago), :invalid? + assert_predicate Topic.new(created_at: 7.days.ago), :valid? + assert_predicate Topic.new(created_at: 1.day.ago), :valid? end def test_validates_inclusion_of_with_symbol @@ -92,7 +92,7 @@ class ExclusionValidationTest < ActiveModel::TestCase %w(abe) end - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is reserved"], p.errors[:karma] p = Person.new @@ -102,7 +102,7 @@ class ExclusionValidationTest < ActiveModel::TestCase %w() end - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 3ddda2154a..2a7088b3e8 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -5,7 +5,7 @@ require "cases/helper" require "models/topic" require "models/person" -class PresenceValidationTest < ActiveModel::TestCase +class FormatValidationTest < ActiveModel::TestCase def teardown Topic.clear_validators! end @@ -16,22 +16,22 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!") assert t.invalid?, "Shouldn't be valid" assert_equal ["is bad data"], t.errors[:title] - assert t.errors[:content].empty? + assert_empty t.errors[:content] t.title = "Validation macros rule!" - assert t.valid? - assert t.errors[:title].empty? + assert_predicate t, :valid? + assert_empty t.errors[:title] assert_raise(ArgumentError) { Topic.validates_format_of(:title, :content) } end def test_validate_format_with_allow_blank Topic.validates_format_of(:title, with: /\AValidation\smacros \w+!\z/, allow_blank: true) - assert Topic.new("title" => "Shouldn't be valid").invalid? - assert Topic.new("title" => "").valid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "Validation macros rule!").valid? + assert_predicate Topic.new("title" => "Shouldn't be valid"), :invalid? + assert_predicate Topic.new("title" => ""), :valid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "Validation macros rule!"), :valid? end # testing ticket #3142 @@ -42,7 +42,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.invalid?, "Shouldn't be valid" assert_equal ["is bad data"], t.errors[:title] - assert t.errors[:content].empty? + assert_empty t.errors[:content] t.title = "-11" assert t.invalid?, "Shouldn't be valid" @@ -58,14 +58,14 @@ class PresenceValidationTest < ActiveModel::TestCase t.title = "1" - assert t.valid? - assert t.errors[:title].empty? + assert_predicate t, :valid? + assert_empty t.errors[:title] end def test_validate_format_with_formatted_message Topic.validates_format_of(:title, with: /\AValid Title\z/, message: "can't be %{value}") t = Topic.new(title: "Invalid title") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end @@ -114,10 +114,10 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "digit" t.content = "Pixies" - assert t.invalid? + assert_predicate t, :invalid? t.content = "1234" - assert t.valid? + assert_predicate t, :valid? end def test_validates_format_of_without_lambda @@ -126,10 +126,10 @@ class PresenceValidationTest < ActiveModel::TestCase t = Topic.new t.title = "characters" t.content = "1234" - assert t.invalid? + assert_predicate t, :invalid? t.content = "Pixies" - assert t.valid? + assert_predicate t, :valid? end def test_validates_format_of_for_ruby_class @@ -137,12 +137,12 @@ class PresenceValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pixies" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is invalid"], p.errors[:karma] p.karma = "1234" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 9cfe189d0e..ccb565c5bd 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -12,6 +12,9 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new I18n.backend.store_translations("en", errors: { messages: { custom: nil } }) + + @original_i18n_full_message = ActiveModel::Errors.i18n_full_message + ActiveModel::Errors.i18n_full_message = true end def teardown @@ -19,6 +22,7 @@ class I18nValidationTest < ActiveModel::TestCase I18n.load_path.replace @old_load_path I18n.backend = @old_backend I18n.backend.reload! + ActiveModel::Errors.i18n_full_message = @original_i18n_full_message end def test_full_message_encoding @@ -31,7 +35,7 @@ class I18nValidationTest < ActiveModel::TestCase def test_errors_full_messages_translates_human_attribute_name_for_model_attributes @person.errors.add(:name, "not found") - assert_called_with(Person, :human_attribute_name, [:name, default: "Name"], returns: "Person's name") do + assert_called_with(Person, :human_attribute_name, ["name", default: "Name"], returns: "Person's name") do assert_equal ["Person's name not found"], @person.errors.full_messages end end @@ -42,6 +46,118 @@ class I18nValidationTest < ActiveModel::TestCase assert_equal ["Field Name empty"], @person.errors.full_messages end + def test_errors_full_messages_doesnt_use_attribute_format_without_config + ActiveModel::Errors.i18n_full_message = false + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } }) + + person = Person.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 + + def test_errors_full_messages_uses_attribute_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } }) + + person = Person.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 + + def test_errors_full_messages_uses_model_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { person: { format: "%{message}" } } } }) + + person = Person.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 + + def test_errors_full_messages_uses_deeply_nested_model_attributes_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank") + assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank") + end + + def test_errors_full_messages_uses_deeply_nested_model_model_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.street', "cannot be blank") + assert_equal "cannot be blank", person.errors.full_message(:'contacts/addresses.country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Contacts/addresses country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } }) + + person = Person.new + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name + ActiveModel::Errors.i18n_full_message = true + + I18n.backend.store_translations("en", activemodel: { + attributes: { 'person/contacts/addresses': { country: "Country" } } + }) + + person = Person.new + assert_equal "Contacts/addresses street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config + ActiveModel::Errors.i18n_full_message = false + + I18n.backend.store_translations("en", activemodel: { + errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } }) + + person = Person.new + assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Contacts[0]/addresses[0] country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + + def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config + ActiveModel::Errors.i18n_full_message = false + + I18n.backend.store_translations("en", activemodel: { + attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } } + }) + + person = Person.new + assert_equal "Contacts[0]/addresses[0] street cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].street', "cannot be blank") + assert_equal "Country cannot be blank", person.errors.full_message(:'contacts[0]/addresses[0].country', "cannot be blank") + end + # ActiveModel::Validations # A set of common cases for ActiveModel::Validations message generation that diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 94df0649a9..daad76759f 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -13,61 +13,61 @@ class InclusionValidationTest < ActiveModel::TestCase def test_validates_inclusion_of_range Topic.validates_inclusion_of(:title, in: "aaa".."bbb") - assert Topic.new("title" => "bbc", "content" => "abc").invalid? - assert Topic.new("title" => "aa", "content" => "abc").invalid? - assert Topic.new("title" => "aaab", "content" => "abc").invalid? - assert Topic.new("title" => "aaa", "content" => "abc").valid? - assert Topic.new("title" => "abc", "content" => "abc").valid? - assert Topic.new("title" => "bbb", "content" => "abc").valid? + assert_predicate Topic.new("title" => "bbc", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aa", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aaab", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "aaa", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "abc", "content" => "abc"), :valid? + assert_predicate Topic.new("title" => "bbb", "content" => "abc"), :valid? end def test_validates_inclusion_of_time_range range_begin = 1.year.ago range_end = Time.now Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.ago).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.ago).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.from_now).invalid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.ago), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.ago), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.from_now), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of_date_range range_begin = 1.year.until(Date.today) range_end = Date.today Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.until(Date.today)).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.until(Date.today)).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)).invalid? - assert Topic.new(title: "aaa", created_at: 1.year.until(Date.today)).valid? - assert Topic.new(title: "aaa", created_at: Date.today).valid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(Date.today)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(Date.today)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 1.year.until(Date.today)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: Date.today), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of_date_time_range range_begin = 1.year.until(DateTime.current) range_end = DateTime.current Topic.validates_inclusion_of(:created_at, in: range_begin..range_end) - assert Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)).invalid? - assert Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)).valid? - assert Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)).invalid? - assert Topic.new(title: "aaa", created_at: range_begin).valid? - assert Topic.new(title: "aaa", created_at: range_end).valid? + assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)), :valid? + assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)), :invalid? + assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid? + assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid? end def test_validates_inclusion_of Topic.validates_inclusion_of(:title, in: %w( a b c d e f g )) - assert Topic.new("title" => "a!", "content" => "abc").invalid? - assert Topic.new("title" => "a b", "content" => "abc").invalid? - assert Topic.new("title" => nil, "content" => "def").invalid? + assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "a b", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => nil, "content" => "def"), :invalid? t = Topic.new("title" => "a", "content" => "I know you are but what am I?") - assert t.valid? + assert_predicate t, :valid? t.title = "uhoh" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is not included in the list"], t.errors[:title] assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) } @@ -81,30 +81,30 @@ class InclusionValidationTest < ActiveModel::TestCase def test_validates_inclusion_of_with_allow_nil Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true) - assert Topic.new("title" => "a!", "content" => "abc").invalid? - assert Topic.new("title" => "", "content" => "abc").invalid? - assert Topic.new("title" => nil, "content" => "abc").valid? + assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => "", "content" => "abc"), :invalid? + assert_predicate Topic.new("title" => nil, "content" => "abc"), :valid? end def test_validates_inclusion_of_with_formatted_message Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list") - assert Topic.new("title" => "a", "content" => "abc").valid? + assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid? t = Topic.new("title" => "uhoh", "content" => "abc") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["option uhoh is not in the list"], t.errors[:title] end def test_validates_inclusion_of_with_within_option Topic.validates_inclusion_of(:title, within: %w( a b c d e f g )) - assert Topic.new("title" => "a", "content" => "abc").valid? + assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid? t = Topic.new("title" => "uhoh", "content" => "abc") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? end def test_validates_inclusion_of_for_ruby_class @@ -112,12 +112,12 @@ class InclusionValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Lifo" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not included in the list"], p.errors[:karma] p.karma = "monkey" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -128,10 +128,10 @@ class InclusionValidationTest < ActiveModel::TestCase t = Topic.new t.title = "wasabi" t.author_name = "sikachu" - assert t.invalid? + assert_predicate t, :invalid? t.title = "elephant" - assert t.valid? + assert_predicate t, :valid? end def test_validates_inclusion_of_with_symbol @@ -144,7 +144,7 @@ class InclusionValidationTest < ActiveModel::TestCase %w() end - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not included in the list"], p.errors[:karma] p = Person.new @@ -154,7 +154,7 @@ class InclusionValidationTest < ActiveModel::TestCase %w(Lifo) end - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 42f76f3e3c..37e10f783c 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -13,153 +13,153 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_with_allow_nil Topic.validates_length_of(:title, is: 5, allow_nil: true) - assert Topic.new("title" => "ab").invalid? - assert Topic.new("title" => "").invalid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "abcde").valid? + assert_predicate Topic.new("title" => "ab"), :invalid? + assert_predicate Topic.new("title" => ""), :invalid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "abcde"), :valid? end def test_validates_length_of_with_allow_blank Topic.validates_length_of(:title, is: 5, allow_blank: true) - assert Topic.new("title" => "ab").invalid? - assert Topic.new("title" => "").valid? - assert Topic.new("title" => nil).valid? - assert Topic.new("title" => "abcde").valid? + assert_predicate Topic.new("title" => "ab"), :invalid? + assert_predicate Topic.new("title" => ""), :valid? + assert_predicate Topic.new("title" => nil), :valid? + assert_predicate Topic.new("title" => "abcde"), :valid? end def test_validates_length_of_using_minimum Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "not" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] t.title = "" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors[:title] t.title = nil - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] end def test_validates_length_of_using_maximum_should_allow_nil Topic.validates_length_of :title, maximum: 10 t = Topic.new - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_minimum Topic.validates_length_of :title, minimum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_maximum Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_maximum Topic.validates_length_of :title, maximum: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_within Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] t.title = nil t.content = nil - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too short (minimum is 3 characters)"], t.errors[:content] t.title = "abe" t.content = "mad" - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_within_with_exclusive_range Topic.validates_length_of(:title, within: 4...10) t = Topic.new("title" => "9 chars!!") - assert t.valid? + assert_predicate t, :valid? t.title = "Now I'm 10" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too long (maximum is 9 characters)"], t.errors[:title] t.title = "Four" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_within Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true t = Topic.new("title" => "abc", "content" => "abcd") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_is Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is the wrong length (should be 5 characters)"], t.errors[:title] t.title = "" - assert t.invalid? + assert_predicate t, :invalid? t.title = nil - assert t.invalid? + assert_predicate t, :invalid? end def test_optionally_validates_length_of_using_is Topic.validates_length_of :title, is: 5, allow_nil: true t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = nil - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_bignum @@ -187,45 +187,45 @@ class LengthValidationTest < ActiveModel::TestCase def test_validates_length_of_custom_errors_for_minimum_with_message Topic.validates_length_of(:title, minimum: 5, message: "boo %{count}") t = Topic.new("title" => "uhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_minimum_with_too_short Topic.validates_length_of(:title, minimum: 5, too_short: "hoo %{count}") t = Topic.new("title" => "uhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_maximum_with_message Topic.validates_length_of(:title, maximum: 5, message: "boo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors[:title] end def test_validates_length_of_custom_errors_for_in Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 10"], t.errors["title"] t = Topic.new("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 20"], t.errors["title"] end def test_validates_length_of_custom_errors_for_maximum_with_too_long Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -233,29 +233,29 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: "too short", too_long: "too long" t = Topic.new(title: "a") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["too short"], t.errors["title"] t = Topic.new(title: "aaaaaa") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["too long"], t.errors["title"] end def test_validates_length_of_custom_errors_for_is_with_message Topic.validates_length_of(:title, is: 5, message: "boo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["boo 5"], t.errors["title"] end def test_validates_length_of_custom_errors_for_is_with_wrong_length Topic.validates_length_of(:title, is: 5, wrong_length: "hoo %{count}") t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -263,11 +263,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二三四" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too short (minimum is 5 characters)"], t.errors["title"] end @@ -275,11 +275,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, maximum: 5 t = Topic.new("title" => "一二三四五", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二34五六" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors["title"] end @@ -287,12 +287,12 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, :content, within: 3..5) t = Topic.new("title" => "一二", "content" => "12三四五六七") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["is too short (minimum is 3 characters)"], t.errors[:title] assert_equal ["is too long (maximum is 5 characters)"], t.errors[:content] t.title = "一二三" t.content = "12三" - assert t.valid? + assert_predicate t, :valid? end def test_optionally_validates_length_of_using_within_utf8 @@ -312,11 +312,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of :title, is: 5 t = Topic.new("title" => "一二345", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "一二345六" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is the wrong length (should be 5 characters)"], t.errors["title"] end @@ -324,11 +324,11 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:approved, is: 4) t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1) - assert t.invalid? - assert t.errors[:approved].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:approved], :any? t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234) - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_for_ruby_class @@ -336,12 +336,12 @@ class LengthValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pix" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma] p.karma = "The Smiths" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end @@ -350,95 +350,95 @@ class LengthValidationTest < ActiveModel::TestCase Topic.validates_length_of(:title, within: 5..Float::INFINITY) t = Topic.new("title" => "1234") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? t.title = "12345" - assert t.valid? + assert_predicate t, :valid? Topic.validates_length_of(:author_name, maximum: Float::INFINITY) - assert t.valid? + assert_predicate t, :valid? t.author_name = "A very long author name that should still be valid." * 100 - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed Topic.validates_length_of :title, maximum: 10, allow_nil: false t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed Topic.validates_length_of :title, maximum: 10, allow_blank: false t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil Topic.validates_length_of :title, minimum: 5, maximum: 10 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? end def test_validates_length_of_using_minimum_0_should_not_allow_nil Topic.validates_length_of :title, minimum: 0 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_length_of_using_is_0_should_not_allow_nil Topic.validates_length_of :title, is: 0 t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? t.title = "" - assert t.valid? + assert_predicate t, :valid? end def test_validates_with_diff_in_option Topic.validates_length_of(:title, is: 5) Topic.validates_length_of(:title, is: 5, if: Proc.new { false }) - assert Topic.new("title" => "david").valid? - assert Topic.new("title" => "david2").invalid? + assert_predicate Topic.new("title" => "david"), :valid? + assert_predicate 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? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate 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.define_method(:max_title_length) { 5 } Topic.validates_length_of :title, maximum: Proc.new(&:max_title_length) t = Topic.new("title" => "valid", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? t.title = "notvalid" - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["is too long (maximum is 5 characters)"], t.errors[:title] t.title = "" - assert t.valid? + assert_predicate 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 5413255e6b..16c44762cb 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -66,7 +66,7 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_of_with_integer_only_and_proc_as_value - Topic.send(:define_method, :allow_only_integers?, lambda { false }) + Topic.define_method(:allow_only_integers?) { false } Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?) invalid!(NIL + BLANK + JUNK) @@ -214,36 +214,36 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_proc - Topic.send(:define_method, :min_approved, lambda { 5 }) + Topic.define_method(:min_approved) { 5 } Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved) invalid!([3, 4]) valid!([5, 6]) ensure - Topic.send(:remove_method, :min_approved) + Topic.remove_method :min_approved end def test_validates_numericality_with_symbol - Topic.send(:define_method, :max_approved, lambda { 5 }) + Topic.define_method(:max_approved) { 5 } Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved invalid!([6]) valid!([4, 5]) ensure - Topic.send(:remove_method, :max_approved) + Topic.remove_method :max_approved end def test_validates_numericality_with_numeric_message Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}" topic = Topic.new("title" => "numeric test", "approved" => 10) - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["smaller than 4"], topic.errors[:approved] Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}" topic = Topic.new("title" => "numeric test", "approved" => 1) - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["greater than 4"], topic.errors[:approved] end @@ -252,23 +252,46 @@ class NumericalityValidationTest < ActiveModel::TestCase p = Person.new p.karma = "Pix" - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["is not a number"], p.errors[:karma] p.karma = "1234" - assert p.valid? + assert_predicate p, :valid? ensure Person.clear_validators! end + def test_validates_numericality_using_value_before_type_cast_if_possible + Topic.validates_numericality_of :price + + topic = Topic.new(price: 50) + + assert_equal "$50.00", topic.price + assert_equal 50, topic.price_before_type_cast + assert_predicate topic, :valid? + end + def test_validates_numericality_with_exponent_number base = 10_000_000_000_000_000 Topic.validates_numericality_of :approved, less_than_or_equal_to: base topic = Topic.new topic.approved = (base + 1).to_s - assert topic.invalid? + assert_predicate topic, :invalid? + end + + def test_validates_numericality_with_object_acting_as_numeric + klass = Class.new do + def to_f + 123.54 + end + end + + Topic.validates_numericality_of :price + topic = Topic.new(price: klass.new) + + assert_predicate topic, :valid? end def test_validates_numericality_with_invalid_args @@ -279,6 +302,13 @@ class NumericalityValidationTest < ActiveModel::TestCase assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, equal_to: "foo" } end + def test_validates_numericality_equality_for_float_and_big_decimal + Topic.validates_numericality_of :approved, equal_to: BigDecimal("65.6") + + invalid!([Float("65.5"), BigDecimal("65.7")], "must be equal to 65.6") + valid!([Float("65.6"), BigDecimal("65.6")]) + end + private def invalid!(values, error = nil) diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index 22c2f0af87..c3eca41070 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -17,25 +17,25 @@ class PresenceValidationTest < ActiveModel::TestCase Topic.validates_presence_of(:title, :content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] assert_equal ["can't be blank"], t.errors[:content] t.title = "something" t.content = " " - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:content] t.content = "like stuff" - assert t.valid? + assert_predicate t, :valid? end def test_accepts_array_arguments Topic.validates_presence_of %w(title content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] assert_equal ["can't be blank"], t.errors[:content] end @@ -43,7 +43,7 @@ class PresenceValidationTest < ActiveModel::TestCase def test_validates_acceptance_of_with_custom_error_using_quotes Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes" p = Person.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last end @@ -51,24 +51,24 @@ class PresenceValidationTest < ActiveModel::TestCase Person.validates_presence_of :karma p = Person.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["can't be blank"], p.errors[:karma] p.karma = "Cold" - assert p.valid? + assert_predicate p, :valid? end def test_validates_presence_of_for_ruby_class_with_custom_reader CustomReader.validates_presence_of :karma p = CustomReader.new - assert p.invalid? + assert_predicate p, :invalid? assert_equal ["can't be blank"], p.errors[:karma] p[:karma] = "Cold" - assert p.valid? + assert_predicate p, :valid? end def test_validates_presence_of_with_allow_nil_option @@ -78,7 +78,7 @@ class PresenceValidationTest < ActiveModel::TestCase assert t.valid?, t.errors.full_messages t.title = "" - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["can't be blank"], t.errors[:title] t.title = " " diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 7f32f5dc74..ae5a875c24 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -19,7 +19,7 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_messages_empty Person.validates :title, presence: { message: "" } person = Person.new - assert !person.valid?, "person should not be valid." + assert_not person.valid?, "person should not be valid." end def test_validates_with_built_in_validation @@ -37,7 +37,7 @@ class ValidatesTest < ActiveModel::TestCase person = Person.new person.title = 123 - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_built_in_validation_and_options @@ -71,7 +71,7 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_if_as_shared_conditions Person.validates :karma, presence: true, email: true, if: :condition_is_false person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_unless_as_local_conditions @@ -84,40 +84,40 @@ class ValidatesTest < ActiveModel::TestCase def test_validates_with_unless_shared_conditions Person.validates :karma, presence: true, email: true, unless: :condition_is_true person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_allow_nil_shared_conditions Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true person = Person.new - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_regexp Person.validates :karma, format: /positive|negative/ person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is invalid"], person.errors[:karma] person.karma = "positive" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_array Person.validates :gender, inclusion: %w(m f) person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is not included in the list"], person.errors[:gender] person.gender = "m" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_range Person.validates :karma, length: 6..20 person = Person.new - assert person.invalid? + assert_predicate person, :invalid? assert_equal ["is too short (minimum is 6 characters)"], person.errors[:karma] person.karma = "something" - assert person.valid? + assert_predicate person, :valid? end def test_validates_with_validator_class_and_options @@ -159,7 +159,7 @@ class ValidatesTest < ActiveModel::TestCase topic = Topic.new topic.title = "What's happening" topic.title_confirmation = "Not this" - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["Y U NO CONFIRM"], topic.errors[:title_confirmation] end end diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 13ef5e6a31..8239792c79 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -65,7 +65,7 @@ class ValidatesWithTest < ActiveModel::TestCase test "with multiple classes" do Topic.validates_with(ValidatorThatAddsErrors, OtherValidatorThatAddsErrors) topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_includes topic.errors[:base], ERROR_MESSAGE assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE end @@ -79,21 +79,21 @@ class ValidatesWithTest < ActiveModel::TestCase validator.expect(:is_a?, false, [String]) Topic.validates_with(validator, if: :condition_is_true, foo: :bar) - assert topic.valid? + assert_predicate topic, :valid? validator.verify end test "validates_with with options" do Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name) topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_includes topic.errors[:base], ERROR_MESSAGE end test "validates_with each validator" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content]) topic = Topic.new title: "Title", content: "Content" - assert topic.invalid? + assert_predicate topic, :invalid? assert_equal ["Value is Title"], topic.errors[:title] assert_equal ["Value is Content"], topic.errors[:content] end @@ -113,28 +113,28 @@ class ValidatesWithTest < ActiveModel::TestCase test "each validator skip nil values if :allow_nil is set to true" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true) topic = Topic.new content: "" - assert topic.invalid? - assert topic.errors[:title].empty? + assert_predicate topic, :invalid? + assert_empty topic.errors[:title] assert_equal ["Value is "], topic.errors[:content] end test "each validator skip blank values if :allow_blank is set to true" do Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true) topic = Topic.new content: "" - assert topic.valid? - assert topic.errors[:title].empty? - assert topic.errors[:content].empty? + assert_predicate topic, :valid? + assert_empty topic.errors[:title] + assert_empty topic.errors[:content] end test "validates_with can validate with an instance method" do Topic.validates :title, with: :my_validation topic = Topic.new title: "foo" - assert topic.valid? - assert topic.errors[:title].empty? + assert_predicate topic, :valid? + assert_empty topic.errors[:title] topic = Topic.new - assert !topic.valid? + assert_not_predicate topic, :valid? assert_equal ["is missing"], topic.errors[:title] end @@ -142,8 +142,8 @@ class ValidatesWithTest < ActiveModel::TestCase Topic.validates :title, :content, with: :my_validation_with_arg topic = Topic.new title: "foo" - assert !topic.valid? - assert topic.errors[:title].empty? + assert_not_predicate topic, :valid? + assert_empty topic.errors[:title] assert_equal ["is missing"], topic.errors[:content] end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index ab8c41bbd0..7776233db5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -30,7 +30,7 @@ class ValidationsTest < ActiveModel::TestCase def test_single_attr_validation_and_error_msg r = Reply.new r.title = "There's no content!" - assert r.invalid? + assert_predicate r, :invalid? assert r.errors[:content].any?, "A reply without content should mark that attribute as invalid" assert_equal ["is Empty"], r.errors["content"], "A reply without content should contain an error" assert_equal 1, r.errors.count @@ -38,7 +38,7 @@ class ValidationsTest < ActiveModel::TestCase def test_double_attr_validation_and_error_msg r = Reply.new - assert r.invalid? + assert_predicate r, :invalid? assert r.errors[:title].any?, "A reply without title should mark that attribute as invalid" assert_equal ["is Empty"], r.errors["title"], "A reply without title should contain an error" @@ -111,8 +111,8 @@ class ValidationsTest < ActiveModel::TestCase def test_errors_empty_after_errors_on_check t = Topic.new - assert t.errors[:id].empty? - assert t.errors.empty? + assert_empty t.errors[:id] + assert_empty t.errors end def test_validates_each @@ -122,7 +122,7 @@ class ValidationsTest < ActiveModel::TestCase hits += 1 end t = Topic.new("title" => "valid", "content" => "whatever") - assert t.invalid? + assert_predicate t, :invalid? assert_equal 4, hits assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] @@ -135,7 +135,7 @@ class ValidationsTest < ActiveModel::TestCase hits += 1 end t = CustomReader.new("title" => "valid", "content" => "whatever") - assert t.invalid? + assert_predicate t, :invalid? assert_equal 4, hits assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] @@ -146,16 +146,16 @@ class ValidationsTest < ActiveModel::TestCase def test_validate_block Topic.validate { errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["will never be valid"], t.errors["title"] end def test_validate_block_with_params Topic.validate { |topic| topic.errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["will never be valid"], t.errors["title"] end @@ -214,7 +214,7 @@ class ValidationsTest < ActiveModel::TestCase def test_errors_conversions Topic.validates_presence_of %w(title content) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? xml = t.errors.to_xml assert_match %r{<errors>}, xml @@ -232,14 +232,14 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_length_of :title, minimum: 2 t = Topic.new("title" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal "can't be blank", t.errors["title"].first Topic.validates_presence_of :title, :author_name Topic.validate { errors.add("author_email_address", "will never be valid") } Topic.validates_length_of :title, :content, minimum: 2 t = Topic.new title: "" - assert t.invalid? + assert_predicate t, :invalid? assert_equal :title, key = t.errors.keys[0] assert_equal "can't be blank", t.errors[key][0] @@ -258,8 +258,8 @@ class ValidationsTest < ActiveModel::TestCase t = Topic.new(title: "") # If block should not fire - assert t.valid? - assert t.author_name.nil? + assert_predicate t, :valid? + assert_predicate t.author_name, :nil? # If block should fire assert t.invalid?(:update) @@ -270,18 +270,18 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_presence_of :title t = Topic.new - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? t.title = "Things are going to change" - assert !t.invalid? + assert_not_predicate t, :invalid? end def test_validation_with_message_as_proc Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase }) t = Topic.new - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["NO BLANKS HERE"], t.errors[:title] end @@ -331,13 +331,13 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates :content, length: { minimum: 10 } topic = Topic.new - assert topic.invalid? + assert_predicate topic, :invalid? assert_equal 3, topic.errors.size topic.title = "Some Title" topic.author_name = "Some Author" topic.content = "Some Content Whose Length is more than 10." - assert topic.valid? + assert_predicate topic, :valid? end def test_validate @@ -381,7 +381,7 @@ class ValidationsTest < ActiveModel::TestCase def test_strict_validation_not_fails Topic.validates :title, strict: true, presence: true - assert Topic.new(title: "hello").valid? + assert_predicate Topic.new(title: "hello"), :valid? end def test_strict_validation_particular_validator @@ -414,7 +414,7 @@ class ValidationsTest < ActiveModel::TestCase def test_validates_with_false_hash_value Topic.validates :title, presence: false - assert Topic.new.valid? + assert_predicate Topic.new, :valid? end def test_strict_validation_error_message @@ -439,19 +439,19 @@ class ValidationsTest < ActiveModel::TestCase duped = topic.dup duped.title = nil - assert duped.invalid? + assert_predicate duped, :invalid? topic.title = nil duped.title = "Mathematics" - assert topic.invalid? - assert duped.valid? + assert_predicate topic, :invalid? + assert_predicate duped, :valid? end def test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter Topic.validates_presence_of(:title, message: proc { |record| "You have failed me for the last time, #{record.author_name}." }) t = Topic.new(author_name: "Admiral") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title] end @@ -459,7 +459,7 @@ class ValidationsTest < ActiveModel::TestCase Topic.validates_presence_of(:title, message: proc { |record, data| "#{data[:attribute]} is missing. You have failed me for the last time, #{record.author_name}." }) t = Topic.new(author_name: "Admiral") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title] end end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index b0af00ee45..db3284f833 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -3,6 +3,11 @@ class Topic include ActiveModel::Validations include ActiveModel::Validations::Callbacks + include ActiveModel::AttributeMethods + include ActiveSupport::NumberHelper + + attribute_method_suffix "_before_type_cast" + define_attribute_method :price def self._validates_default_keys super | [ :message ] @@ -10,6 +15,7 @@ class Topic attr_accessor :title, :author_name, :content, :approved, :created_at attr_accessor :after_validation_performed + attr_writer :price after_validation :perform_after_validation @@ -38,4 +44,12 @@ class Topic def my_validation_with_arg(attr) errors.add attr, "is missing" unless send(attr) end + + def price + number_to_currency @price + end + + def attribute_before_type_cast(attr) + instance_variable_get(:"@#{attr}") + end end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index e98fd8a0a1..bb1b187694 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -7,6 +7,7 @@ class User define_model_callbacks :create has_secure_password + has_secure_password :recovery_password, validations: false - attr_accessor :password_digest + attr_accessor :password_digest, :recovery_password_digest end diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 9da004ffcc..96bf3ef10a 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -8,5 +8,6 @@ class Visitor has_secure_password(validations: false) - attr_accessor :password_digest, :password_confirmation + attr_accessor :password_digest + attr_reader :password_confirmation end |