diff options
Diffstat (limited to 'activemodel')
130 files changed, 2596 insertions, 629 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 7483704212..13ea7d0bf3 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,37 +1,3 @@ -* Fix regression in numericality validator when comparing Decimal and Float input - values with more scale than the schema. - *Bradley Priest* -* Fix methods `#keys`, `#values` in `ActiveModel::Errors`. - - Change `#keys` to only return the keys that don't have empty messages. - - Change `#values` to only return the not empty values. - - Example: - - # Before - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [:name] - person.errors.values # => [[]] - - # After - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [] - person.errors.values # => [] - - *bogdanvlviv* - - -Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activemodel/CHANGELOG.md) for previous changes. +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..1cb3add0fc 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2017 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 77f5761993..1aaf4813ea 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -246,16 +246,16 @@ Source code can be downloaded as part of the Rails project on GitHub Active Model is released under the MIT license: -* http://www.opensource.org/licenses/MIT +* https://opensource.org/licenses/MIT == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/activemodel/Rakefile b/activemodel/Rakefile index d60f6d9997..d39f50a962 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rake/testtask" task default: :test diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 43f1e09c77..a070a2898c 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| @@ -18,5 +20,10 @@ Gem::Specification.new do |s| s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"] s.require_path = "lib" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activemodel", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md" + } + s.add_dependency "activesupport", version end diff --git a/activemodel/bin/test b/activemodel/bin/test index 470ce93f10..c53377cc97 100755 --- a/activemodel/bin/test +++ b/activemodel/bin/test @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true COMPONENT_ROOT = File.expand_path("..", __dir__) require_relative "../../tools/test" diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index a2892e9ea9..bc10d6b4b9 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -28,6 +30,8 @@ require "active_model/version" module ActiveModel extend ActiveSupport::Autoload + autoload :Attribute + autoload :Attributes autoload :AttributeAssignment autoload :AttributeMethods autoload :BlockValidator, "active_model/validator" @@ -43,6 +47,7 @@ module ActiveModel autoload :SecurePassword autoload :Serialization autoload :Translation + autoload :Type autoload :Validations autoload :Validator diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb new file mode 100644 index 0000000000..27f3e38e31 --- /dev/null +++ b/activemodel/lib/active_model/attribute.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/duplicable" + +module ActiveModel + class Attribute # :nodoc: + class << self + def from_database(name, value, type) + FromDatabase.new(name, value, type) + end + + def from_user(name, value, type, original_attribute = nil) + FromUser.new(name, value, type, original_attribute) + end + + def with_cast_value(name, value, type) + WithCastValue.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) + end + end + + attr_reader :name, :value_before_type_cast, :type + + # This method should not be called directly. + # Use #from_database or #from_user + def initialize(name, value_before_type_cast, type, original_attribute = nil) + @name = name + @value_before_type_cast = value_before_type_cast + @type = type + @original_attribute = original_attribute + end + + def value + # `defined?` is cheaper than `||=` when we get back falsy values + @value = type_cast(value_before_type_cast) unless defined?(@value) + @value + end + + def original_value + if assigned? + original_attribute.original_value + else + type_cast(value_before_type_cast) + end + end + + def value_for_database + type.serialize(value) + end + + def changed? + changed_from_assignment? || changed_in_place? + end + + def changed_in_place? + has_been_read? && type.changed_in_place?(original_value_for_database, value) + end + + def forgetting_assignment + with_value_from_database(value_for_database) + end + + def with_value_from_user(value) + type.assert_valid_value(value) + self.class.from_user(name, value, type, original_attribute || self) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def with_cast_value(value) + self.class.with_cast_value(name, value, type) + end + + def with_type(type) + if changed_in_place? + with_value_from_user(value).with_type(type) + else + self.class.new(name, value_before_type_cast, type, original_attribute) + end + end + + def type_cast(*) + raise NotImplementedError + end + + def initialized? + true + end + + def came_from_user? + false + end + + def has_been_read? + defined?(@value) + end + + def ==(other) + self.class == other.class && + name == other.name && + value_before_type_cast == other.value_before_type_cast && + type == other.type + end + alias eql? == + + def hash + [self.class, name, value_before_type_cast, type].hash + end + + def init_with(coder) + @name = coder["name"] + @value_before_type_cast = coder["value_before_type_cast"] + @type = coder["type"] + @original_attribute = coder["original_attribute"] + @value = coder["value"] if coder.map.key?("value") + end + + def encode_with(coder) + coder["name"] = name + coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil? + coder["type"] = type if type + coder["original_attribute"] = original_attribute if original_attribute + coder["value"] = value if defined?(@value) + end + + protected + + attr_reader :original_attribute + alias_method :assigned?, :original_attribute + + def original_value_for_database + if assigned? + original_attribute.original_value_for_database + else + _original_value_for_database + end + end + + private + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end + end + + def changed_from_assignment? + assigned? && type.changed?(original_value, value, value_before_type_cast) + end + + def _original_value_for_database + type.serialize(original_value) + end + + class FromDatabase < Attribute # :nodoc: + def type_cast(value) + type.deserialize(value) + end + + def _original_value_for_database + value_before_type_cast + end + end + + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.cast(value) + end + + def came_from_user? + !type.value_constructed_by_mass_assignment?(value_before_type_cast) + end + end + + class WithCastValue < Attribute # :nodoc: + def type_cast(value) + value + end + + def changed_in_place? + false + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type.default_value) + end + + def type_cast(*) + nil + end + + def with_type(type) + self.class.with_cast_value(name, nil, type) + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + UNINITIALIZED_ORIGINAL_VALUE = Object.new + + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def original_value + UNINITIALIZED_ORIGINAL_VALUE + end + + def value_for_database + end + + def initialized? + false + end + + def forgetting_assignment + dup + end + + def with_type(type) + self.class.new(name, type) + end + end + + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue + end +end diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb new file mode 100644 index 0000000000..a5dc6188d2 --- /dev/null +++ b/activemodel/lib/active_model/attribute/user_provided_default.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "active_model/attribute" + +module ActiveModel + class Attribute # :nodoc: + class UserProvidedDefault < FromUser # :nodoc: + def initialize(name, value, type, database_default) + @user_provided_value = value + super(name, value, type, database_default) + end + + def value_before_type_cast + if user_provided_value.is_a?(Proc) + @memoized_value_before_type_cast ||= user_provided_value.call + else + @user_provided_value + end + end + + def with_type(type) + self.class.new(name, user_provided_value, type, original_attribute) + end + + def marshal_dump + result = [ + name, + value_before_type_cast, + type, + original_attribute, + ] + result << value if defined?(@value) + result + end + + def marshal_load(values) + name, user_provided_value, type, original_attribute, value = values + @name = name + @user_provided_value = user_provided_value + @type = type + @original_attribute = original_attribute + if values.length == 5 + @value = value + end + end + + protected + + attr_reader :user_provided_value + end + end +end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index b3b39bf7ae..888a431e5f 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "concurrent/map" -require "mutex_m" module ActiveModel # Raised when an attribute is not defined. @@ -327,13 +328,11 @@ module ActiveModel attribute_method_matchers_cache.clear end - def generated_attribute_methods #:nodoc: - @generated_attribute_methods ||= Module.new { - extend Mutex_m - }.tap { |mod| include mod } - end - private + def generated_attribute_methods + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } + end + def instance_method_already_implemented?(method_name) generated_attribute_methods.method_defined?(method_name) end diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb new file mode 100644 index 0000000000..f55613ecd5 --- /dev/null +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "active_support/core_ext/hash/indifferent_access" + +module ActiveModel + class AttributeMutationTracker # :nodoc: + OPTION_NOT_GIVEN = Object.new + + def initialize(attributes) + @attributes = attributes + @forced_changes = Set.new + end + + def changed_values + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = attributes[attr_name].original_value + end + end + end + + def changes + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + change = change_to_attribute(attr_name) + if change + result[attr_name] = change + end + end + end + + def change_to_attribute(attr_name) + attr_name = attr_name.to_s + if changed?(attr_name) + [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + end + end + + def changed_attribute_names + attr_names.select { |attr| changed?(attr) } + end + + def any_changes? + attr_names.any? { |attr| changed?(attr) } + end + + def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) + attr_name = attr_name.to_s + forced_changes.include?(attr_name) || + attributes[attr_name].changed? && + (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) && + (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to) + end + + def changed_in_place?(attr_name) + attributes[attr_name.to_s].changed_in_place? + end + + def forget_change(attr_name) + attr_name = attr_name.to_s + attributes[attr_name] = attributes[attr_name].forgetting_assignment + forced_changes.delete(attr_name) + end + + def original_value(attr_name) + attributes[attr_name.to_s].original_value + end + + def force_change(attr_name) + forced_changes << attr_name.to_s + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + + attr_reader :attributes, :forced_changes + + private + + def attr_names + attributes.keys + end + end + + class NullMutationTracker # :nodoc: + include Singleton + + def changed_values(*) + {} + end + + def changes(*) + {} + end + + def change_to_attribute(attr_name) + end + + def any_changes?(*) + false + end + + def changed?(*) + false + end + + def changed_in_place?(*) + false + end + + def forget_change(*) + end + + def original_value(*) + end + end +end diff --git a/activemodel/lib/active_model/attribute_set.rb b/activemodel/lib/active_model/attribute_set.rb new file mode 100644 index 0000000000..a890ee3932 --- /dev/null +++ b/activemodel/lib/active_model/attribute_set.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/deep_dup" +require "active_model/attribute_set/builder" +require "active_model/attribute_set/yaml_encoder" + +module ActiveModel + class AttributeSet # :nodoc: + delegate :each_value, :fetch, :except, to: :attributes + + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def []=(name, value) + attributes[name] = value + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def keys + attributes.each_key.select { |name| self[name].initialized? } + end + + if defined?(JRUBY_VERSION) + # This form is significantly faster on JRuby, and this is one of our biggest hotspots. + # https://github.com/jruby/jruby/pull/2562 + def fetch_value(name, &block) + self[name].value(&block) + end + else + def fetch_value(name) + self[name].value { |n| yield n if block_given? } + end + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def write_cast_value(name, value) + attributes[name] = self[name].with_cast_value(value) + end + + def freeze + @attributes.freeze + super + end + + def deep_dup + self.class.allocate.tap do |copy| + copy.instance_variable_set(:@attributes, attributes.deep_dup) + end + end + + def initialize_dup(_) + @attributes = attributes.dup + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if key?(key) + write_from_database(key, nil) + end + end + + def accessed + attributes.select { |_, attr| attr.has_been_read? }.keys + end + + def map(&block) + new_attributes = attributes.transform_values(&block) + AttributeSet.new(new_attributes) + end + + def ==(other) + attributes == other.attributes + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb new file mode 100644 index 0000000000..bf2d06b48a --- /dev/null +++ b/activemodel/lib/active_model/attribute_set/builder.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require "active_model/attribute" + +module ActiveModel + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types, :default_attributes + + def initialize(types, default_attributes = {}) + @types = types + @default_attributes = default_attributes + end + + def build_from_database(values = {}, additional_types = {}) + attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes) + AttributeSet.new(attributes) + end + end + end + + class LazyAttributeHash # :nodoc: + delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize + + def initialize(types, values, additional_types, default_attributes, delegate_hash = {}) + @types = types + @values = values + @additional_types = additional_types + @materialized = false + @delegate_hash = delegate_hash + @default_attributes = default_attributes + end + + def key?(key) + delegate_hash.key?(key) || values.key?(key) || types.key?(key) + end + + def [](key) + delegate_hash[key] || assign_default_value(key) + end + + def []=(key, value) + if frozen? + raise RuntimeError, "Can't modify frozen hash" + end + delegate_hash[key] = value + end + + def deep_dup + dup.tap do |copy| + copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup)) + end + end + + def initialize_dup(_) + @delegate_hash = Hash[delegate_hash] + super + end + + def select + keys = types.keys | values.keys | delegate_hash.keys + keys.each_with_object({}) do |key, hash| + attribute = self[key] + if yield(key, attribute) + hash[key] = attribute + end + end + end + + def ==(other) + if other.is_a?(LazyAttributeHash) + materialize == other.materialize + else + materialize == other + end + end + + def marshal_dump + [@types, @values, @additional_types, @default_attributes, @delegate_hash] + end + + def marshal_load(values) + if values.is_a?(Hash) + empty_hash = {}.freeze + initialize(empty_hash, empty_hash, empty_hash, empty_hash, values) + @materialized = true + else + initialize(*values) + end + end + + protected + + attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes + + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash + end + + private + + def assign_default_value(name) + type = additional_types.fetch(name, types[name]) + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + delegate_hash[name] = Attribute.from_database(name, value, type) + elsif types.key?(name) + attr = default_attributes[name] + if attr + delegate_hash[name] = attr.dup + else + delegate_hash[name] = Attribute.uninitialized(name, type) + end + end + end + end +end diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb new file mode 100644 index 0000000000..4ea945b956 --- /dev/null +++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module ActiveModel + class AttributeSet + # Attempts to do more intelligent YAML dumping of an + # ActiveModel::AttributeSet to reduce the size of the resulting string + class YAMLEncoder # :nodoc: + def initialize(default_types) + @default_types = default_types + end + + def encode(attribute_set, coder) + coder["concise_attributes"] = attribute_set.each_value.map do |attr| + if attr.type.equal?(default_types[attr.name]) + attr.with_type(nil) + else + attr + end + end + end + + def decode(coder) + if coder["attributes"] + coder["attributes"] + else + attributes_hash = Hash[coder["concise_attributes"].map do |attr| + if attr.type.nil? + attr = attr.with_type(default_types[attr.name]) + end + [attr.name, attr] + end] + AttributeSet.new(attributes_hash) + end + end + + protected + + attr_reader :default_types + end + end +end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb new file mode 100644 index 0000000000..046ae67ad7 --- /dev/null +++ b/activemodel/lib/active_model/attributes.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require "active_model/attribute_set" +require "active_model/attribute/user_provided_default" + +module ActiveModel + module Attributes #:nodoc: + extend ActiveSupport::Concern + include ActiveModel::AttributeMethods + + included do + attribute_method_suffix "=" + class_attribute :attribute_types, :_default_attributes, instance_accessor: false + self.attribute_types = Hash.new(Type.default_value) + self._default_attributes = AttributeSet.new({}) + end + + module ClassMethods + def attribute(name, type = Type::Value.new, **options) + name = name.to_s + if type.is_a?(Symbol) + type = ActiveModel::Type.lookup(type, **options.except(:default)) + end + self.attribute_types = attribute_types.merge(name => type) + define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type) + define_attribute_method(name) + end + + private + + def define_method_attribute=(name) + safe_name = name.unpack("h*".freeze).first + ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end + + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED + + def define_default_attribute(name, value, type) + self._default_attributes = _default_attributes.deep_dup + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + else + default_attribute = Attribute::UserProvidedDefault.new( + name, + value, + type, + _default_attributes.fetch(name.to_s) { nil }, + ) + end + _default_attributes[name] = default_attribute + end + end + + def initialize(*) + @attributes = self.class._default_attributes.deep_dup + super + end + + private + + def write_attribute(attr_name, value) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + @attributes.write_from_user(name, value) + value + end + + def attribute(attr_name) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + @attributes.fetch_value(name) + end + + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end + end + + module AttributeMethods #:nodoc: + AttrNames = Module.new { + def self.set_name_cache(name, value) + const_name = "ATTR_#{name}" + unless const_defined? const_name + const_set const_name, value.dup.freeze + end + end + } + end +end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 835e6f7716..8fa9680cb1 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/extract_options" module ActiveModel @@ -101,8 +103,8 @@ module ActiveModel # end # end # - # NOTE: +method_name+ passed to `define_model_callbacks` must not end with - # `!`, `?` or `=`. + # NOTE: +method_name+ passed to define_model_callbacks must not end with + # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>. def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 12687c70d3..cdc1282817 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # == Active \Model \Conversion # diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index dc81f74779..0044fde6c5 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + require "active_support/hash_with_indifferent_access" require "active_support/core_ext/object/duplicable" +require "active_model/attribute_mutation_tracker" +require "active_model/attribute_set" module ActiveModel # == Active \Model \Dirty @@ -128,13 +132,30 @@ module ActiveModel attribute_method_affix prefix: "restore_", suffix: "!" end + def initialize_dup(other) # :nodoc: + super + if self.class.respond_to?(:_default_attributes) + @attributes = self.class._default_attributes.map do |attr| + attr.with_value_from_user(@attributes.fetch_value(attr.name)) + end + end + @mutations_from_database = nil + end + + def changes_applied # :nodoc: + _prepare_changes + @mutations_before_last_save = mutations_from_database + forget_attribute_assignments + @mutations_from_database = nil + end + # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise. # # person.changed? # => false # person.name = 'bob' # person.changed? # => true def changed? - changed_attributes.present? + mutations_from_database.any_changes? end # Returns an array with the name of the attributes with unsaved changes. @@ -143,7 +164,52 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - changed_attributes.keys + mutations_from_database.changed_attribute_names + end + + # Handles <tt>*_changed?</tt> for +method_missing+. + def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: + !!mutations_from_database.changed?(attr) && + (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && + (from == OPTION_NOT_GIVEN || from == attribute_was(attr)) + end + + # Handles <tt>*_was</tt> for +method_missing+. + def attribute_was(attr) # :nodoc: + mutations_from_database.original_value(attr) + end + + # Handles <tt>*_previously_changed?</tt> for +method_missing+. + def attribute_previously_changed?(attr) #:nodoc: + mutations_before_last_save.changed?(attr) + end + + # Restore all previous data of the provided attributes. + def restore_attributes(attributes = changed) + attributes.each { |attr| restore_attribute! attr } + end + + # Clears all dirty data: current changes and previous changes. + def clear_changes_information + @mutations_before_last_save = nil + forget_attribute_assignments + @mutations_from_database = nil + end + + def clear_attribute_changes(attr_names) + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end + end + + # Returns a hash of the attributes with unsaved changes indicating their original + # values like <tt>attr => original value</tt>. + # + # person.name # => "bob" + # person.name = 'robert' + # person.changed_attributes # => {"name" => "bob"} + def changed_attributes + mutations_from_database.changed_values.freeze end # Returns a hash of changed attributes indicating their original @@ -153,7 +219,8 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] + _prepare_changes + mutations_from_database.changes end # Returns a hash of attributes that were changed before the model was saved. @@ -163,110 +230,82 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new - end - - # Returns a hash of the attributes with unsaved changes indicating their original - # values like <tt>attr => original value</tt>. - # - # person.name # => "bob" - # person.name = 'robert' - # person.changed_attributes # => {"name" => "bob"} - def changed_attributes - @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new - end - - # Handles <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!changes_include?(attr) && - (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) - end - - # Handles <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) # :nodoc: - attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) + mutations_before_last_save.changes end - # Handles <tt>*_previously_changed?</tt> for +method_missing+. - def attribute_previously_changed?(attr) #:nodoc: - previous_changes_include?(attr) - end - - # Restore all previous data of the provided attributes. - def restore_attributes(attributes = changed) - attributes.each { |attr| restore_attribute! attr } + def attribute_changed_in_place?(attr_name) # :nodoc: + mutations_from_database.changed_in_place?(attr_name) end private - - # Returns +true+ if attr_name is changed, +false+ otherwise. - def changes_include?(attr_name) - attributes_changed_by_setter.include?(attr_name) + def clear_attribute_change(attr_name) + mutations_from_database.forget_change(attr_name) end - alias attribute_changed_by_setter? changes_include? - # Returns +true+ if attr_name were changed before the model was saved, - # +false+ otherwise. - def previous_changes_include?(attr_name) - previous_changes.include?(attr_name) + def mutations_from_database + unless defined?(@mutations_from_database) + @mutations_from_database = nil + end + + unless defined?(@attributes) + @_pseudo_attributes = true + @attributes = AttributeSet.new( + Hash.new { |h, attr| + h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value) + } + ) + end + + @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes) end - # Removes current changes and makes them accessible through +previous_changes+. - def changes_applied # :doc: - @previously_changed = changes - @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + def forget_attribute_assignments + @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes) end - # Clears all dirty data: current changes and previous changes. - def clear_changes_information # :doc: - @previously_changed = ActiveSupport::HashWithIndifferentAccess.new - @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + def mutations_before_last_save + @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) + [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - previous_changes[attr] if attribute_previously_changed?(attr) + mutations_before_last_save.change_to_attribute(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) - return if attribute_changed?(attr) - - begin - value = _read_attribute(attr) - value = value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError + attr = attr.to_s + mutations_from_database.force_change(attr).tap do + @attributes[attr] if defined?(@_pseudo_attributes) end - - set_attribute_was(attr, value) end # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) - __send__("#{attr}=", changed_attributes[attr]) + __send__("#{attr}=", attribute_was(attr)) clear_attribute_changes([attr]) end end - # This is necessary because `changed_attributes` might be overridden in - # other implementations (e.g. in `ActiveRecord`) - alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: - - # Force an attribute to have a particular "before" value - def set_attribute_was(attr, old_value) - attributes_changed_by_setter[attr] = old_value + def _prepare_changes + if defined?(@_pseudo_attributes) + changed.each do |attr| + @attributes.write_from_user(attr, _read_attribute(attr)) + end + end end - # Remove changes information for the provided attributes. - def clear_attribute_changes(attributes) # :doc: - attributes_changed_by_setter.except!(*attributes) + def _clone_attribute(attr) + value = _read_attribute(attr) + value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + value end end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 942b4fa9bb..275e3f1313 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/conversions" require "active_support/core_ext/string/inflections" require "active_support/core_ext/object/deep_dup" @@ -93,6 +95,18 @@ module ActiveModel @details = other.details.dup end + # Merges the errors from <tt>other</tt>. + # + # other - The ActiveModel::Errors instance. + # + # Examples + # + # person.errors.merge!(other) + def merge!(other) + @messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 } + @details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 } + end + # Clear the error messages. # # person.errors.full_messages # => ["name cannot be nil"] @@ -308,9 +322,13 @@ module ActiveModel # person.errors.added? :name, :too_long # => false # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) - message = message.call if message.respond_to?(:call) - message = normalize_message(attribute, message, options) - self[attribute].include? message + if message.is_a? Symbol + self.details[attribute].map { |e| e[:error] }.include? message + else + message = message.call if message.respond_to?(:call) + message = normalize_message(attribute, message, options) + self[attribute].include? message + end end # Returns all the full error messages in an array. @@ -384,21 +402,19 @@ module ActiveModel type = options.delete(:message) if options[:message].is_a?(Symbol) if @base.class.respond_to?(:i18n_scope) - defaults = @base.class.lookup_ancestors.map do |klass| - [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", - :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] + i18n_scope = @base.class.i18n_scope.to_s + defaults = @base.class.lookup_ancestors.flat_map do |klass| + [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}", + :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ] end + defaults << :"#{i18n_scope}.errors.messages.#{type}" else defaults = [] end - defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" - defaults.compact! - defaults.flatten! - key = defaults.shift defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb index 45ab8a2ca1..4b37f80c52 100644 --- a/activemodel/lib/active_model/forbidden_attributes_protection.rb +++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # Raised when forbidden attributes are used for mass assignment. # diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 67bdfaa643..cef5441e4a 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt> def self.gem_version @@ -5,8 +7,8 @@ module ActiveModel end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 291a545528..b7ceabb59a 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Lint # == Active \Model \Lint \Tests @@ -27,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 @@ -42,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" @@ -54,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 @@ -66,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 @@ -77,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 @@ -98,13 +100,13 @@ module ActiveModel # If localization is used, the strings should be localized for the current # locale. If no error is present, the method should return an empty array. def test_errors_aref - assert model.respond_to?(:errors), "The model should respond to errors" + assert_respond_to model, :errors assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array" end private def model - assert @model.respond_to?(:to_model), "The object should respond to to_model" + assert_respond_to @model, :to_model @model.to_model end diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb index 945a5402a3..fc52cd4fdf 100644 --- a/activemodel/lib/active_model/model.rb +++ b/activemodel/lib/active_model/model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # == Active \Model \Basic \Model # diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 9ac56526a2..dfccd03cd8 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/except" require "active_support/core_ext/module/introspection" -require "active_support/core_ext/module/remove_method" +require "active_support/core_ext/module/redefine_method" module ActiveModel class Name @@ -216,7 +218,7 @@ module ActiveModel # provided method below, or rolling your own is required. module Naming def self.extended(base) #:nodoc: - base.remove_possible_method :model_name + base.silence_redefinition_of_method :model_name base.delegate :model_name, to: :class end diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb index 1671eb7bd4..a9cdabba00 100644 --- a/activemodel/lib/active_model/railtie.rb +++ b/activemodel/lib/active_model/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model" require "rails" diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 1c0fe92bc0..86f051f5ce 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActiveModel module SecurePassword extend ActiveSupport::Concern - # BCrypt hash function can handle maximum 72 characters, and if we pass - # password of length more than 72 characters it ignores extra characters. + # BCrypt hash function can handle maximum 72 bytes, and if we pass + # password of length more than 72 bytes it ignores extra characters. # Hence need to put a restriction on password length. MAX_PASSWORD_LENGTH_ALLOWED = 72 @@ -18,7 +20,7 @@ module ActiveModel # # The following validations are added automatically: # * Password must be present on creation - # * Password length should be less than or equal to 72 characters + # * Password length should be less than or equal to 72 bytes # * Confirmation of password (using a +password_confirmation+ attribute) # # If password confirmation validation is not needed, simply leave out the diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 77834f26fc..47cb81bee5 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 205b84ddb4..25e1541d66 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/json" module ActiveModel diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 35fc7cf743..f3d0d3dc27 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # == Active \Model \Translation # diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb index 095801d8f0..1d7a26fff5 100644 --- a/activemodel/lib/active_model/type.rb +++ b/activemodel/lib/active_model/type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/type/helpers" require "active_model/type/value" @@ -22,7 +24,7 @@ module ActiveModel class << self attr_accessor :registry # :nodoc: - # Add a new type to the registry, allowing it to be get through ActiveModel::Type#lookup + # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup def register(type_name, klass = nil, **options, &block) registry.register(type_name, klass, **options, &block) end @@ -30,6 +32,10 @@ module ActiveModel def lookup(*args, **kwargs) # :nodoc: registry.lookup(*args, **kwargs) end + + def default_value # :nodoc: + @default_value ||= Value.new + end end register(:big_integer, Type::BigInteger) diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb index 3b629682fe..89e43bcc5f 100644 --- a/activemodel/lib/active_model/type/big_integer.rb +++ b/activemodel/lib/active_model/type/big_integer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/type/integer" module ActiveModel diff --git a/activemodel/lib/active_model/type/binary.rb b/activemodel/lib/active_model/type/binary.rb index 819e4e4a96..dc2eca18be 100644 --- a/activemodel/lib/active_model/type/binary.rb +++ b/activemodel/lib/active_model/type/binary.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class Binary < Value # :nodoc: diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb index f2a47370a3..bcdbab0343 100644 --- a/activemodel/lib/active_model/type/boolean.rb +++ b/activemodel/lib/active_model/type/boolean.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type # == Active \Model \Type \Boolean diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb index eefd080351..8cecc16d0f 100644 --- a/activemodel/lib/active_model/type/date.rb +++ b/activemodel/lib/active_model/type/date.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class Date < Value # :nodoc: diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb index 5cb0077e45..9641bf45ee 100644 --- a/activemodel/lib/active_model/type/date_time.rb +++ b/activemodel/lib/active_model/type/date_time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class DateTime < Value # :nodoc: @@ -10,6 +12,10 @@ module ActiveModel :datetime end + def serialize(value) + super(cast(value)) + end + private def cast_value(value) diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index e6805c5f6b..e8ee18c00e 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bigdecimal/util" module ActiveModel diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb index 4d0d2771a0..9dbe32e5a6 100644 --- a/activemodel/lib/active_model/type/float.rb +++ b/activemodel/lib/active_model/type/float.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class Float < Value # :nodoc: diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb index 82cd9ebe98..403f0a9e6b 100644 --- a/activemodel/lib/active_model/type/helpers.rb +++ b/activemodel/lib/active_model/type/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/type/helpers/accepts_multiparameter_time" require "active_model/type/helpers/numeric" require "active_model/type/helpers/mutable" diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb index f783d286c5..ad891f841e 100644 --- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb +++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type module Helpers # :nodoc: all @@ -19,6 +21,10 @@ module ActiveModel end end + define_method(:value_constructed_by_mass_assignment?) do |value| + value.is_a?(Hash) + end + define_method(:value_from_multiparameter_assignment) do |values_hash| defaults.each do |k, v| values_hash[k] ||= v diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb index f3a17a1698..1cbea644c4 100644 --- a/activemodel/lib/active_model/type/helpers/mutable.rb +++ b/activemodel/lib/active_model/type/helpers/mutable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type module Helpers # :nodoc: all diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb index 275822b738..16e14f9e5f 100644 --- a/activemodel/lib/active_model/type/helpers/numeric.rb +++ b/activemodel/lib/active_model/type/helpers/numeric.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type module Helpers # :nodoc: all diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb index 53cf7c6029..250c4021c6 100644 --- a/activemodel/lib/active_model/type/helpers/time_value.rb +++ b/activemodel/lib/active_model/type/helpers/time_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/time/zones" module ActiveModel diff --git a/activemodel/lib/active_model/type/immutable_string.rb b/activemodel/lib/active_model/type/immutable_string.rb index 58268540e5..826bd7038f 100644 --- a/activemodel/lib/active_model/type/immutable_string.rb +++ b/activemodel/lib/active_model/type/immutable_string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class ImmutableString < Value # :nodoc: diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 106b5d966c..fe396998a3 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActiveModel module Type class Integer < Value # :nodoc: include Helpers::Numeric # Column storage size in bytes. - # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + # 4 bytes means an integer as opposed to smallint etc. DEFAULT_LIMIT = 4 def initialize(*) diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb index 2d5dd366eb..7272d7b0c5 100644 --- a/activemodel/lib/active_model/type/registry.rb +++ b/activemodel/lib/active_model/type/registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel # :stopdoc: module Type diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb index 850cab962b..36f13945b1 100644 --- a/activemodel/lib/active_model/type/string.rb +++ b/activemodel/lib/active_model/type/string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/type/immutable_string" module ActiveModel diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index 54d6214e81..ad7ba0351a 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class Time < Value # :nodoc: diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb index 7e9ae92245..a8ea6a2c22 100644 --- a/activemodel/lib/active_model/type/value.rb +++ b/activemodel/lib/active_model/type/value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Type class Value @@ -84,6 +86,10 @@ module ActiveModel false end + def value_constructed_by_mass_assignment?(_value) # :nodoc: + false + end + def map(value) # :nodoc: yield value end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index ae1d69f685..7f14d102dd 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/extract_options" require "active_support/core_ext/hash/keys" require "active_support/core_ext/hash/except" @@ -162,14 +164,14 @@ module ActiveModel if options.key?(:on) options = options.dup + options[:on] = Array(options[:on]) options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - !(Array(options[:on]) & Array(o.validation_context)).empty? + !(options[:on] & Array(o.validation_context)).empty? } end - args << options - set_callback(:validate, *args, &block) + set_callback(:validate, *args, options, &block) end # List all validators that are being used to validate the model using diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 4618f46e30..385d9f27e0 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations # == \Active \Model Absence Validator diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index a26c37daa5..f35e4dec7f 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class AcceptanceValidator < EachValidator # :nodoc: diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 4e94422cf1..887d31ae2a 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations # == Active \Model \Validation \Callbacks @@ -52,15 +54,18 @@ module ActiveModel # person.valid? # => true # person.name # => "bob" def before_validation(*args, &block) - options = args.last - if options.is_a?(Hash) && options[:on] - options[:if] = Array(options[:if]) + options = args.extract_options! + + if options.key?(:on) + options = options.dup options[:on] = Array(options[:on]) + options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - options[:on].include? o.validation_context + !(options[:on] & Array(o.validation_context)).empty? } end - set_callback(:validation, :before, *args, &block) + + set_callback(:validation, :before, *args, options, &block) end # Defines a callback that will get called right after validation. @@ -91,15 +96,18 @@ module ActiveModel # person.status # => true def after_validation(*args, &block) options = args.extract_options! + options = options.dup options[:prepend] = true - options[:if] = Array(options[:if]) - if options[:on] + + if options.key?(:on) options[:on] = Array(options[:on]) + options[:if] = Array(options[:if]) options[:if].unshift ->(o) { - options[:on].include? o.validation_context + !(options[:on] & Array(o.validation_context)).empty? } end - set_callback(:validation, :after, *(args << options), &block) + + set_callback(:validation, :after, *args, options, &block) end end diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb index 18f1056e2b..0b9b5ce6a1 100644 --- a/activemodel/lib/active_model/validations/clusivity.rb +++ b/activemodel/lib/active_model/validations/clusivity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/range" module ActiveModel diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 03585bf5e1..1b5d5b09ab 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class ConfirmationValidator < EachValidator # :nodoc: @@ -7,7 +9,7 @@ module ActiveModel end def validate_each(record, attribute, value) - if (confirmed = record.send("#{attribute}_confirmation")) + unless (confirmed = record.send("#{attribute}_confirmation")).nil? unless confirmation_value_equal?(record, attribute, value, confirmed) human_attribute_name = record.class.human_attribute_name(attribute) record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name)) diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index b7156ba802..3be7ab6ba8 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/validations/clusivity" module ActiveModel diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 98b3f6a8e5..7c3f091473 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class FormatValidator < EachValidator # :nodoc: diff --git a/activemodel/lib/active_model/validations/helper_methods.rb b/activemodel/lib/active_model/validations/helper_methods.rb index 2176115334..730173f2f9 100644 --- a/activemodel/lib/active_model/validations/helper_methods.rb +++ b/activemodel/lib/active_model/validations/helper_methods.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations module HelperMethods # :nodoc: diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index c6c5bae649..3104e7e329 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model/validations/clusivity" module ActiveModel diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 940c58f3a7..d6c80b2c5d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: @@ -29,8 +31,8 @@ module ActiveModel keys.each do |key| value = options[key] - unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY - raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity" + unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc) + raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc" end end end @@ -43,6 +45,12 @@ module ActiveModel next unless check_value = options[key] if !value.nil? || skip_nil_check?(key) + case check_value + when Proc + check_value = check_value.call(record) + when Symbol + check_value = record.send(check_value) + end next if value_length.send(validity_check, check_value) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index fb053a4c4e..31750ba78e 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class NumericalityValidator < EachValidator # :nodoc: diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index ce4106a5e1..8787a75afa 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveModel module Validations class PresenceValidator < EachValidator # :nodoc: diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index a8b958e974..e28e7e9219 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/slice" module ActiveModel @@ -152,7 +154,7 @@ module ActiveModel # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. def _validates_default_keys - [:if, :unless, :on, :allow_blank, :allow_nil , :strict] + [:if, :unless, :on, :allow_blank, :allow_nil, :strict] end def _parse_validates_options(options) diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 9227dd06ff..d777ac836e 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/extract_options" module ActiveModel diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 6c11981e1d..e17c3ca7b3 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/module/anonymous" module ActiveModel diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index 6e7fd227fd..dd817f5639 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "gem_version" module ActiveModel diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index fa41d1b95f..5ecf0a69c4 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/hash/indifferent_access" require "active_support/hash_with_indifferent_access" diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 4767accb7c..0cfc6f4b6b 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class ModelWithAttributes @@ -116,7 +118,7 @@ class AttributeMethodsTest < ActiveModel::TestCase test "#define_attribute_method does not generate attribute method if already defined in attribute module" do klass = Class.new(ModelWithAttributes) - klass.generated_attribute_methods.module_eval do + klass.send(:generated_attribute_methods).module_eval do def foo "<3" end @@ -218,7 +220,7 @@ class AttributeMethodsTest < ActiveModel::TestCase 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 @@ -253,7 +255,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 new file mode 100644 index 0000000000..b868dba743 --- /dev/null +++ b/activemodel/test/cases/attribute_set_test.rb @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +require "cases/helper" +require "active_model/attribute_set" + +module ActiveModel + class AttributeSetTest < ActiveModel::TestCase + test "building a new set from raw attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: "1.1", bar: "2.2") + + assert_equal 1, attributes[:foo].value + assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name + end + + test "building with custom types" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new }) + + assert_equal 3.3, attributes[:foo].value + assert_equal 4, attributes[:bar].value + end + + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: "3.3") + + assert_equal "3.3", attributes[:foo].value_before_type_cast + assert_nil attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name + end + + test "duping creates a new hash, but does not dup the attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: "foo") + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.dup + duped.write_from_database(:foo, 2) + duped[:bar].value << "bar" + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal "foobar", attributes[:bar].value + assert_equal "foobar", duped[:bar].value + end + + test "deep_duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: "foo") + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.deep_dup + duped.write_from_database(:foo, 2) + duped[:bar].value << "bar" + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal "foo", attributes[:bar].value + assert_equal "foobar", duped[:bar].value + end + + test "freezing cloned set does not freeze original" do + attributes = AttributeSet.new({}) + clone = attributes.clone + + clone.freeze + + assert_predicate clone, :frozen? + assert_not_predicate attributes, :frozen? + end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: "1.1", bar: "2.2") + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "to_hash maintains order" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: "2.2", bar: "3.3") + + attributes[:bar] + hash = attributes.to_h + + assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1.1", bar: "2.2") + + assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert_predicate attributes[:foo], :initialized? + assert_not_predicate attributes[:bar], :initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert attributes.key?(:foo) + assert_not attributes.key?(:bar) + end + + test "unknown attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.key?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: "1.1", bar: "2.2") + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) { "hello" } + end + + test "fetch_value returns nil for unknown attributes when types has a default" do + types = Hash.new(Type::Value.new) + builder = AttributeSet::Builder.new(types) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:wibble) { "hello" } + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + "!" } + assert_equal "bar!", value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + test "the primary_key is always initialized" do + defaults = { foo: Attribute.from_user(:foo, nil, nil) } + builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults) + attributes = builder.build_from_database + + assert attributes.key?(:foo) + assert_equal [:foo], attributes.keys + assert_predicate attributes[:foo], :initialized? + end + + class MyType + def cast(value) + return if value.nil? + value + " from user" + end + + def deserialize(value) + return if value.nil? + value + " from database" + end + + def assert_valid_value(*) + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + test "freezing doesn't prevent the set from materializing" do + builder = AttributeSet::Builder.new(foo: Type::String.new) + attributes = builder.build_from_database(foo: "1") + + attributes.freeze + assert_equal({ foo: "1" }, attributes.to_hash) + end + + test "marshaling 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") + + assert_equal [], attributes.accessed + + attributes.fetch_value(:foo) + + assert_equal [:foo], attributes.accessed + end + + test "#map returns a new attribute set with the changes applied" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + new_attributes = attributes.map do |attr| + attr.with_cast_value(attr.value + 1) + end + + assert_equal 2, new_attributes.fetch_value(:foo) + assert_equal 3, new_attributes.fetch_value(:bar) + end + + test "comparison for equality is correctly implemented" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + attributes2 = builder.build_from_database(foo: "1", bar: "2") + attributes3 = builder.build_from_database(foo: "2", bar: "2") + + assert_equal attributes, attributes2 + assert_not_equal attributes2, attributes3 + end + + 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 new file mode 100644 index 0000000000..ea2b0efd11 --- /dev/null +++ b/activemodel/test/cases/attribute_test.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveModel + class AttributeTest < ActiveModel::TestCase + setup do + @type = Minitest::Mock.new + end + + teardown do + assert @type.verify + end + + test "from_database + read type casts from database" do + @type.expect(:deserialize, "type cast from database", ["a value"]) + attribute = Attribute.from_database(nil, "a value", @type) + + type_cast_value = attribute.value + + assert_equal "type cast from database", type_cast_value + end + + test "from_user + read type casts from user" do + @type.expect(:cast, "type cast from user", ["a value"]) + attribute = Attribute.from_user(nil, "a value", @type) + + type_cast_value = attribute.value + + assert_equal "type cast from user", type_cast_value + end + + test "reading memoizes the value" do + @type.expect(:deserialize, "from the database", ["whatever"]) + attribute = Attribute.from_database(nil, "whatever", @type) + + type_cast_value = attribute.value + second_read = attribute.value + + assert_equal "from the database", type_cast_value + assert_same type_cast_value, second_read + end + + test "reading memoizes falsy values" do + @type.expect(:deserialize, false, ["whatever"]) + attribute = Attribute.from_database(nil, "whatever", @type) + + attribute.value + attribute.value + end + + test "read_before_typecast returns the given value" do + attribute = Attribute.from_database(nil, "raw value", @type) + + raw_value = attribute.value_before_type_cast + + assert_equal "raw value", raw_value + end + + test "from_database + read_for_database type casts to and from database" do + @type.expect(:deserialize, "read from database", ["whatever"]) + @type.expect(:serialize, "ready for database", ["read from database"]) + attribute = Attribute.from_database(nil, "whatever", @type) + + serialize = attribute.value_for_database + + assert_equal "ready for database", serialize + end + + test "from_user + read_for_database type casts from the user to the database" do + @type.expect(:cast, "read from user", ["whatever"]) + @type.expect(:serialize, "ready for database", ["read from user"]) + attribute = Attribute.from_user(nil, "whatever", @type) + + serialize = attribute.value_for_database + + assert_equal "ready for database", serialize + end + + test "duping dups the value" do + @type.expect(:deserialize, "type cast".dup, ["a value"]) + attribute = Attribute.from_database(nil, "a value", @type) + + value_from_orig = attribute.value + value_from_clone = attribute.dup.value + value_from_orig << " foo" + + assert_equal "type cast foo", value_from_orig + assert_equal "type cast", value_from_clone + end + + test "duping does not dup the value if it is not dupable" do + @type.expect(:deserialize, false, ["a value"]) + attribute = Attribute.from_database(nil, "a value", @type) + + assert_same attribute.value, attribute.dup.value + end + + test "duping does not eagerly type cast if we have not yet type cast" do + attribute = Attribute.from_database(nil, "a value", @type) + attribute.dup + end + + class MyType + def cast(value) + value + " from user" + end + + def deserialize(value) + value + " from database" + end + + def assert_valid_value(*) + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + + test "attributes equal other attributes with the same constructor arguments" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Integer.new) + assert_equal first, second + end + + test "attributes do not equal attributes with different names" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:bar, 1, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different types" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Float.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different values" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 2, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes of other classes" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_user(:foo, 1, Type::Integer.new) + assert_not_equal first, second + end + + test "an attribute has not been read by default" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + assert_not_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_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) + + 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_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) + + assert_not_predicate unchanged, :changed? + end + + test "an attribute can not be mutated if it has not been read, + and skips expensive calculations" do + type_which_raises_from_all_methods = Object.new + attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) + + assert_not_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_predicate attribute, :changed_in_place? + assert_predicate attribute, :changed? + end + + test "an attribute can forget its changes" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + changed = attribute.with_value_from_user("foo") + forgotten = changed.forgetting_assignment + + assert changed.changed? # sanity check + assert_not_predicate forgotten, :changed? + end + + test "with_value_from_user validates the value" do + type = Type::Value.new + type.define_singleton_method(:assert_valid_value) do |value| + if value == 1 + raise ArgumentError + end + end + + attribute = Attribute.from_database(:foo, 1, type) + assert_equal 1, attribute.value + assert_equal 2, attribute.with_value_from_user(2).value + assert_raises ArgumentError do + attribute.with_value_from_user(1) + end + end + + test "with_type preserves mutations" do + attribute = Attribute.from_database(:foo, "".dup, Type::Value.new) + attribute.value << "1" + + assert_equal 1, attribute.with_type(Type::Integer.new).value + end + end +end diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb new file mode 100644 index 0000000000..c991176389 --- /dev/null +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require "cases/helper" + +class AttributesDirtyTest < ActiveModel::TestCase + class DirtyModel + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Dirty + attribute :name, :string + attribute :color, :string + attribute :size, :integer + + def save + changes_applied + end + + def reload + clear_changes_information + end + end + + setup do + @model = DirtyModel.new + end + + test "setting attribute will result in change" do + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? + @model.name = "Ringo" + assert_predicate @model, :changed? + assert_predicate @model, :name_changed? + end + + test "list of changed attribute keys" do + assert_equal [], @model.changed + @model.name = "Paul" + assert_equal ["name"], @model.changed + end + + test "changes to attribute values" do + assert !@model.changes["name"] + @model.name = "John" + assert_equal [nil, "John"], @model.changes["name"] + end + + test "checking if an attribute has changed to a particular value" do + @model.name = "Ringo" + assert @model.name_changed?(from: nil, to: "Ringo") + assert_not @model.name_changed?(from: "Pete", to: "Ringo") + assert @model.name_changed?(to: "Ringo") + assert_not @model.name_changed?(to: "Pete") + assert @model.name_changed?(from: nil) + assert_not @model.name_changed?(from: "Pete") + end + + test "changes accessible through both strings and symbols" do + @model.name = "David" + assert_not_nil @model.changes[:name] + assert_not_nil @model.changes["name"] + end + + test "be consistent with symbols arguments after the changes are applied" do + @model.name = "David" + assert @model.attribute_changed?(:name) + @model.save + @model.name = "Rafael" + assert @model.attribute_changed?(:name) + end + + test "attribute mutation" do + @model.name = "Yam" + @model.save + assert_not_predicate @model, :name_changed? + @model.name.replace("Hadad") + assert_predicate @model, :name_changed? + end + + test "resetting attribute" do + @model.name = "Bob" + @model.restore_name! + assert_nil @model.name + 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_predicate @model, :color_changed? + @model.save + assert_not_predicate @model, :color_changed? + assert_not_predicate @model, :changed? + @model.color = "red" + 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_predicate @model, :changed? + @model.save + assert_not_predicate @model, :changed? + assert_not_predicate @model, :name_changed? + end + + test "saving should preserve previous changes" do + @model.name = "Jericho Cane" + @model.save + assert_equal [nil, "Jericho Cane"], @model.previous_changes["name"] + end + + test "setting new attributes should not affect previous changes" do + @model.name = "Jericho Cane" + @model.save + @model.name = "DudeFella ManGuy" + assert_equal [nil, "Jericho Cane"], @model.name_previous_change + end + + test "saving should preserve model's previous changed status" do + @model.name = "Jericho Cane" + @model.save + assert_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.save + + @model.name = "John" + assert_equal({ "name" => "Paul" }, @model.changed_attributes) + end + + test "changing the same attribute multiple times retains the correct original value" do + @model.name = "Otto" + @model.save + @model.name = "DudeFella ManGuy" + @model.name = "Mr. Manfredgensonton" + assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change + assert_equal @model.name_was, "Otto" + end + + test "using attribute_will_change! with a symbol" do + @model.size = 1 + assert_predicate @model, :size_changed? + end + + test "reload should reset all changes" do + @model.name = "Dmitry" + @model.name_changed? + @model.save + @model.name = "Bob" + + assert_equal [nil, "Dmitry"], @model.previous_changes["name"] + assert_equal "Dmitry", @model.changed_attributes["name"] + + @model.reload + + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes + end + + test "restore_attributes should restore all previous data" do + @model.name = "Dmitry" + @model.color = "Red" + @model.save + @model.name = "Bob" + @model.color = "White" + + @model.restore_attributes + + assert_not_predicate @model, :changed? + assert_equal "Dmitry", @model.name + assert_equal "Red", @model.color + end + + test "restore_attributes can restore only some attributes" do + @model.name = "Dmitry" + @model.color = "Red" + @model.save + @model.name = "Bob" + @model.color = "White" + + @model.restore_attributes(["name"]) + + assert_predicate @model, :changed? + assert_equal "Dmitry", @model.name + assert_equal "White", @model.color + end + + test "changing the attribute reports a change only when the cast value changes" do + @model.size = "2.3" + @model.save + @model.size = "2.1" + + assert_equal false, @model.changed? + + @model.size = "5.1" + + assert_equal true, @model.changed? + assert_equal true, @model.size_changed? + assert_equal({ "size" => [2, 5] }, @model.changes) + end +end diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb new file mode 100644 index 0000000000..7c1d813ce0 --- /dev/null +++ b/activemodel/test/cases/attributes_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveModel + class AttributesTest < ActiveModel::TestCase + class ModelForAttributesTest + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :integer_field, :integer + attribute :string_field, :string + attribute :decimal_field, :decimal + attribute :string_with_default, :string, default: "default string" + attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) } + attribute :boolean_field, :boolean + end + + class ChildModelForAttributesTest < ModelForAttributesTest + end + + class GrandchildModelForAttributesTest < ChildModelForAttributesTest + attribute :integer_field, :string + end + + test "properties assignment" do + data = ModelForAttributesTest.new( + integer_field: "2.3", + string_field: "Rails FTW", + decimal_field: "12.3", + boolean_field: "0" + ) + + assert_equal 2, data.integer_field + assert_equal "Rails FTW", data.string_field + assert_equal BigDecimal("12.3"), data.decimal_field + assert_equal "default string", data.string_with_default + assert_equal Date.new(2016, 1, 1), data.date_field + assert_equal false, data.boolean_field + + data.integer_field = 10 + data.string_with_default = nil + data.boolean_field = "1" + + assert_equal 10, data.integer_field + assert_nil data.string_with_default + assert_equal true, data.boolean_field + end + + test "nonexistent attribute" do + assert_raise ActiveModel::UnknownAttributeError do + ModelForAttributesTest.new(nonexistent: "nonexistent") + end + end + + test "children inherit attributes" do + data = ChildModelForAttributesTest.new(integer_field: "4.4") + + assert_equal 4, data.integer_field + end + + test "children can override parents" do + data = GrandchildModelForAttributesTest.new(integer_field: "4.4") + + assert_equal "4.4", data.integer_field + end + + 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 f85cd7dec4..1ec12d8222 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class CallbacksTest < ActiveModel::TestCase @@ -83,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 diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 4a93347abc..347896ed50 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/contact" require "models/helicopter" diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index fdd18d7601..f769eb0da1 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -1,14 +1,17 @@ +# frozen_string_literal: true + 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 @@ -38,6 +41,15 @@ class DirtyTest < ActiveModel::TestCase @size = val end + def status + @status + end + + def status=(val) + status_will_change! unless val == @status + @status = val + end + def save changes_applied end @@ -52,11 +64,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 @@ -96,83 +108,94 @@ class DirtyTest < ActiveModel::TestCase end test "attribute mutation" do - @model.instance_variable_set("@name", "Yam") - assert !@model.name_changed? + @model.instance_variable_set("@name", "Yam".dup) + 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 @@ -199,7 +222,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 @@ -213,8 +236,12 @@ 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 + + test "model can be dup-ed without Attributes" do + assert @model.dup + end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 43aee5a814..fe351f922d 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/string/strip" require "yaml" @@ -81,7 +83,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 @@ -126,8 +128,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 @@ -221,6 +223,13 @@ class ErrorsTest < ActiveModel::TestCase assert !person.errors.added?(:name) end + test "added? returns false when checking for an error by symbol and a different error with same message is present" do + I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) + person = Person.new + person.errors.add(:name, :wrong) + assert !person.errors.added?(:name, :used) + end + test "size calculates the number of error messages" do person = Person.new person.errors.add(:name, "cannot be blank") @@ -311,7 +320,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) } @@ -362,7 +371,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 @@ -375,6 +384,18 @@ class ErrorsTest < ActiveModel::TestCase assert_equal [:name], person.errors.details.keys end + test "merge errors" do + errors = ActiveModel::Errors.new(Person.new) + errors.add(:name, :invalid) + + person = Person.new + person.errors.add(:name, :blank) + person.errors.merge!(errors) + + assert_equal({ name: ["can't be blank", "is invalid"] }, person.errors.messages) + assert_equal({ name: [{ error: :blank }, { error: :invalid }] }, person.errors.details) + end + test "errors are marshalable" do errors = ActiveModel::Errors.new(Person.new) errors.add(:name, :invalid) diff --git a/activemodel/test/cases/forbidden_attributes_protection_test.rb b/activemodel/test/cases/forbidden_attributes_protection_test.rb index d8cc72e662..0fd0a2f8ee 100644 --- a/activemodel/test/cases/forbidden_attributes_protection_test.rb +++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/hash/indifferent_access" require "models/account" diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index eeb5c85a48..91fb9d0a7c 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_model" # Show backtraces for deprecated behavior for quicker cleanup. diff --git a/activemodel/test/cases/lint_test.rb b/activemodel/test/cases/lint_test.rb index 7a817d7c01..d62c80b71a 100644 --- a/activemodel/test/cases/lint_test.rb +++ b/activemodel/test/cases/lint_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class LintTest < ActiveModel::TestCase diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb index ba87cd1506..b24d7e3571 100644 --- a/activemodel/test/cases/model_test.rb +++ b/activemodel/test/cases/model_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class ModelTest < ActiveModel::TestCase diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index d5cb1a62bc..009f1f47af 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/contact" require "models/sheep" diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb index a56b26b5ee..ff5022e960 100644 --- a/activemodel/test/cases/railtie_test.rb +++ b/activemodel/test/cases/railtie_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/testing/isolation" diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 77ce86f392..d19e81a119 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/user" require "models/visitor" diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index f78efd2f0c..9002982e7f 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/object/instance_variables" diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index d15ba64eb0..aae98c9fe4 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/contact" require "active_support/core_ext/object/instance_variables" diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 9972f9daea..cd75afec9e 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" diff --git a/activemodel/test/cases/type/big_integer_test.rb b/activemodel/test/cases/type/big_integer_test.rb index 56002b7cc6..0fa0200df4 100644 --- a/activemodel/test/cases/type/big_integer_test.rb +++ b/activemodel/test/cases/type/big_integer_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/binary_test.rb b/activemodel/test/cases/type/binary_test.rb index e9c2ccfca4..3221a73e49 100644 --- a/activemodel/test/cases/type/binary_test.rb +++ b/activemodel/test/cases/type/binary_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/boolean_test.rb b/activemodel/test/cases/type/boolean_test.rb index 92e5aebfb7..2de0f53640 100644 --- a/activemodel/test/cases/type/boolean_test.rb +++ b/activemodel/test/cases/type/boolean_test.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type 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 0cc90e99d3..e8cf178612 100644 --- a/activemodel/test/cases/type/date_test.rb +++ b/activemodel/test/cases/type/date_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb index 75a7fc686e..60f62becc2 100644 --- a/activemodel/test/cases/type/date_time_test.rb +++ b/activemodel/test/cases/type/date_time_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb index c3b43725cc..c0cf6ce590 100644 --- a/activemodel/test/cases/type/decimal_test.rb +++ b/activemodel/test/cases/type/decimal_test.rb @@ -1,27 +1,28 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type class DecimalTest < ActiveModel::TestCase def test_type_cast_decimal type = Decimal.new - assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.cast(123.0) - assert_equal BigDecimal.new("1"), type.cast(:"1") + assert_equal BigDecimal("0"), type.cast(BigDecimal("0")) + assert_equal BigDecimal("123"), type.cast(123.0) + assert_equal BigDecimal("1"), type.cast(:"1") end def test_type_cast_decimal_from_invalid_string type = Decimal.new assert_nil type.cast("") - assert_equal BigDecimal.new("1"), type.cast("1ignore") - assert_equal BigDecimal.new("0"), type.cast("bad1") - assert_equal BigDecimal.new("0"), type.cast("bad") + assert_equal BigDecimal("1"), type.cast("1ignore") + assert_equal BigDecimal("0"), type.cast("bad1") + assert_equal BigDecimal("0"), type.cast("bad") end def test_type_cast_decimal_from_float_with_large_precision type = Decimal.new(precision: ::Float::DIG + 2) - assert_equal BigDecimal.new("123.0"), type.cast(123.0) + assert_equal BigDecimal("123.0"), type.cast(123.0) end def test_type_cast_from_float_with_unspecified_precision @@ -47,7 +48,7 @@ module ActiveModel def test_type_cast_decimal_from_object_responding_to_d value = Object.new def value.to_d - BigDecimal.new("1") + BigDecimal("1") end type = Decimal.new assert_equal BigDecimal("1"), type.cast(value) diff --git a/activemodel/test/cases/type/float_test.rb b/activemodel/test/cases/type/float_test.rb index 8026d63ad5..28318e06f8 100644 --- a/activemodel/test/cases/type/float_test.rb +++ b/activemodel/test/cases/type/float_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/immutable_string_test.rb b/activemodel/test/cases/type/immutable_string_test.rb index 23e58974fb..751f753ddb 100644 --- a/activemodel/test/cases/type/immutable_string_test.rb +++ b/activemodel/test/cases/type/immutable_string_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb index a91144036b..8c5d18c9b3 100644 --- a/activemodel/test/cases/type/integer_test.rb +++ b/activemodel/test/cases/type/integer_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" require "active_support/core_ext/numeric/time" module ActiveModel diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb index 927b6d0307..0633ea2538 100644 --- a/activemodel/test/cases/type/registry_test.rb +++ b/activemodel/test/cases/type/registry_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb index 47d412e27e..825c8bb246 100644 --- a/activemodel/test/cases/type/string_test.rb +++ b/activemodel/test/cases/type/string_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type @@ -14,7 +15,7 @@ module ActiveModel test "cast strings are mutable" do type = Type::String.new - s = "foo" + s = "foo".dup assert_equal false, type.cast(s).frozen? assert_equal false, s.frozen? diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb index 0cc4d33caa..f7102d1e97 100644 --- a/activemodel/test/cases/type/time_test.rb +++ b/activemodel/test/cases/type/time_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/type/value_test.rb b/activemodel/test/cases/type/value_test.rb index d8b3e7f164..55b5d9d584 100644 --- a/activemodel/test/cases/type/value_test.rb +++ b/activemodel/test/cases/type/value_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "cases/helper" -require "active_model/type" module ActiveModel module Type diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb index 833f694c5a..8bc4f4723a 100644 --- a/activemodel/test/cases/validations/absence_validation_test.rb +++ b/activemodel/test/cases/validations/absence_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/person" @@ -15,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 @@ -32,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 @@ -41,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 @@ -49,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 fbd994e914..7662f996ae 100644 --- a/activemodel/test/cases/validations/acceptance_validation_test.rb +++ b/activemodel/test/cases/validations/acceptance_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -13,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 @@ -69,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 @@ -81,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 f2e4a5946d..ff3cf61746 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Dog @@ -57,6 +59,18 @@ class DogValidatorWithOnCondition < Dog def set_after_validation_marker; history << "after_validation_marker" ; end end +class DogValidatorWithOnMultipleCondition < Dog + before_validation :set_before_validation_marker_on_context_a, on: :context_a + before_validation :set_before_validation_marker_on_context_b, on: :context_b + after_validation :set_after_validation_marker_on_context_a, on: :context_a + after_validation :set_after_validation_marker_on_context_b, on: :context_b + + def set_before_validation_marker_on_context_a; history << "before_validation_marker on context_a"; end + def set_before_validation_marker_on_context_b; history << "before_validation_marker on context_b"; end + def set_after_validation_marker_on_context_a; history << "after_validation_marker on context_a" ; end + def set_after_validation_marker_on_context_b; history << "after_validation_marker on context_b" ; end +end + class DogValidatorWithIfCondition < Dog before_validation :set_before_validation_marker1, if: -> { true } before_validation :set_before_validation_marker2, if: -> { false } @@ -96,6 +110,37 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal [], d.history end + def test_on_multiple_condition_is_respected_for_validation_with_matching_context + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:context_a) + assert_equal ["before_validation_marker on context_a", "after_validation_marker on context_a"], d.history + + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:context_b) + assert_equal ["before_validation_marker on context_b", "after_validation_marker on context_b"], d.history + + d = DogValidatorWithOnMultipleCondition.new + d.valid?([:context_a, :context_b]) + assert_equal([ + "before_validation_marker on context_a", + "before_validation_marker on context_b", + "after_validation_marker on context_a", + "after_validation_marker on context_b" + ], d.history) + end + + def test_on_multiple_condition_is_respected_for_validation_without_matching_context + d = DogValidatorWithOnMultipleCondition.new + d.valid?(:save) + assert_equal [], d.history + end + + def test_on_multiple_condition_is_respected_for_validation_without_context + d = DogValidatorWithOnMultipleCondition.new + d.valid? + assert_equal [], d.history + end + def test_before_validation_and_after_validation_callbacks_should_be_called d = DogWithMethodCallbacks.new d.valid? diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 048d27446e..1704db9a48 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -11,75 +13,63 @@ 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_unless_validation_using_method_true - # When the method returns true - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true) + 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.valid? - assert_empty t.errors[:title] + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? + assert_equal ["hoo 5"], t.errors["title"] end - def test_if_validation_using_method_false - # When the method returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not) + def test_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.valid? - assert_empty t.errors[:title] + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? + assert_equal ["hoo 5"], t.errors["title"] end - def test_unless_validation_using_method_false - # When the method returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not) + def test_unless_validation_using_method_true + # When the method returns true + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? - assert_equal ["hoo 5"], t.errors["title"] + assert_predicate t, :valid? + assert_empty t.errors[:title] end - def test_if_validation_using_string_true - # When the evaluated string returns true - ActiveSupport::Deprecation.silence do - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1") - end + 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.invalid? - assert t.errors[:title].any? - assert_equal ["hoo 5"], t.errors["title"] + assert_predicate t, :valid? + assert_empty t.errors[:title] end - def test_unless_validation_using_string_true - # When the evaluated string returns true - ActiveSupport::Deprecation.silence do - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") - end + def test_unless_validation_using_array_of_true_and_felse_methods + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: [:condition_is_true, :condition_is_false]) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end - def test_if_validation_using_string_false - # When the evaluated string returns false - ActiveSupport::Deprecation.silence do - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") - end + def test_if_validation_using_method_false + # When the method returns false + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.valid? + assert_predicate t, :valid? assert_empty t.errors[:title] end - def test_unless_validation_using_string_false - # When the evaluated string returns false - ActiveSupport::Deprecation.silence do - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false") - end + def test_unless_validation_using_method_false + # When the method returns false + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_false) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") - assert t.invalid? - assert t.errors[:title].any? + assert_predicate t, :invalid? + assert_predicate t.errors[:title], :any? assert_equal ["hoo 5"], t.errors["title"] end @@ -88,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 @@ -98,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 @@ -107,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 @@ -116,32 +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 - # previous implementation of validates_presence_of eval'd the - # string with the wrong binding, this regression test is to - # ensure that it works correctly - def test_validation_with_if_as_string - Topic.validates_presence_of(:title) - ActiveSupport::Deprecation.silence do - Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')") - end - - t = Topic.new - assert t.invalid?, "A topic without a title should not be valid" - assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author" - - t.title = "Just a title" - assert t.valid?, "A topic with a basic title should be valid" - - t.title = "A very important title" - assert t.invalid?, "A topic with an important title, but without an author, should not be valid" - assert t.errors[:author_name].any?, "A topic with an 'important' title should require an author" + def test_validation_using_conbining_if_true_and_unless_true_conditions + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_true) + t = Topic.new("title" => "uhohuhoh", "content" => "whatever") + assert_predicate t, :valid? + assert_empty t.errors[:title] + end - t.author_name = "Hubert J. Farnsworth" - assert t.valid?, "A topic with an important title and author should be valid" + def test_validation_using_conbining_if_true_and_unless_false_conditions + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true, unless: :condition_is_false) + t = Topic.new("title" => "uhohuhoh", "content" => "whatever") + assert_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 7ddf3ad273..8603a8ac5c 100644 --- a/activemodel/test/cases/validations/confirmation_validation_test.rb +++ b/activemodel/test/cases/validations/confirmation_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -12,27 +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_predicate t, :valid? + + t.approved_confirmation = false + assert_predicate t, :invalid? + + t.approved_confirmation = true + assert_predicate t, :valid? end def test_validates_confirmation_of_for_ruby_class @@ -40,12 +55,12 @@ 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 @@ -62,7 +77,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase Topic.validates_confirmation_of(:title) t = Topic.new("title" => "We should be confirmed", "title_confirmation" => "") - assert t.invalid? + assert_predicate t, :invalid? assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation] ensure I18n.load_path.replace @old_load_path @@ -107,13 +122,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 06ae4fbecd..50bd47065c 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/core_ext/numeric/time" @@ -12,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 @@ -22,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 @@ -33,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 @@ -42,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 @@ -58,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 @@ -90,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 @@ -100,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 f5b1ad721c..2a7088b3e8 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" require "models/person" -class PresenceValidationTest < ActiveModel::TestCase +class FormatValidationTest < ActiveModel::TestCase def teardown Topic.clear_validators! end @@ -14,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 @@ -40,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" @@ -56,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 @@ -112,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 @@ -124,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 @@ -135,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_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb index f049ee26e8..d3e44945db 100644 --- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index f28cfc0ef5..9cfe189d0e 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 5aa43ea4a9..daad76759f 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "active_support/all" @@ -11,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) } @@ -79,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 @@ -110,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 @@ -126,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 @@ -142,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 @@ -152,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 95ee87b401..774a2cde74 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -11,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 @@ -185,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 @@ -231,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 @@ -261,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 @@ -273,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 @@ -285,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 @@ -310,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 @@ -322,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 @@ -334,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 @@ -348,64 +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_predicate t, :valid? + + t.title = "notvalid" + 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_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.validates_length_of :title, maximum: Proc.new(&:max_title_length) + + t = Topic.new("title" => "valid", "content" => "whatever") + assert_predicate t, :valid? + + t.title = "notvalid" + 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_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 c0158e075f..01b78ae72e 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -18,7 +20,7 @@ class NumericalityValidationTest < ActiveModel::TestCase INTEGER_STRINGS = %w(0 +0 -0 10 +10 -10 0090 -090) FLOATS = [0.0, 10.0, 10.5, -10.5, -0.0001] + FLOAT_STRINGS INTEGERS = [0, 10, -10] + INTEGER_STRINGS - BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal.new(bd) } + BIGDECIMAL = BIGDECIMAL_STRINGS.collect! { |bd| BigDecimal(bd) } JUNK = ["not a number", "42 not a number", "0xdeadbeef", "0xinvalidhex", "0Xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"] INFINITY = [1.0 / 0.0] @@ -57,7 +59,7 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_of_with_integer_only_and_symbol_as_value - Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not + Topic.validates_numericality_of :approved, only_integer: :condition_is_false invalid!(NIL + BLANK + JUNK) valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) @@ -79,10 +81,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_greater_than_using_differing_numeric_types - Topic.validates_numericality_of :approved, greater_than: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18") - invalid!([-97.18, BigDecimal.new("97.18"), BigDecimal("-97.18")], "must be greater than 97.18") - valid!([97.19, 98, BigDecimal.new("98"), BigDecimal.new("97.19")]) + invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18") + valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")]) end def test_validates_numericality_with_greater_than_using_string_value @@ -100,10 +102,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_greater_than_or_equal_using_differing_numeric_types - Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18") - invalid!([-97.18, 97.17, 97, BigDecimal.new("97.17"), BigDecimal.new("-97.18")], "must be greater than or equal to 97.18") - valid!([97.18, 98, BigDecimal.new("97.19")]) + invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18") + valid!([97.18, 98, BigDecimal("97.19")]) end def test_validates_numericality_with_greater_than_or_equal_using_string_value @@ -121,10 +123,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_equal_to_using_differing_numeric_types - Topic.validates_numericality_of :approved, equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18") invalid!([-97.18], "must be equal to 97.18") - valid!([BigDecimal.new("97.18")]) + valid!([BigDecimal("97.18")]) end def test_validates_numericality_with_equal_to_using_string_value @@ -142,10 +144,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_less_than_using_differing_numeric_types - Topic.validates_numericality_of :approved, less_than: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18") - invalid!([97.18, BigDecimal.new("97.18")], "must be less than 97.18") - valid!([-97.0, 97.0, -97, 97, BigDecimal.new("-97"), BigDecimal.new("97")]) + invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18") + valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")]) end def test_validates_numericality_with_less_than_using_string_value @@ -163,10 +165,10 @@ class NumericalityValidationTest < ActiveModel::TestCase end def test_validates_numericality_with_less_than_or_equal_to_using_differing_numeric_types - Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal.new("97.18") + Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18") invalid!([97.19, 98], "must be less than or equal to 97.18") - valid!([-97.18, BigDecimal.new("-97.18"), BigDecimal.new("97.18")]) + valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")]) end def test_validates_numericality_with_less_than_or_equal_using_string_value @@ -235,13 +237,13 @@ class NumericalityValidationTest < ActiveModel::TestCase 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 @@ -250,12 +252,12 @@ 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 @@ -266,7 +268,7 @@ class NumericalityValidationTest < ActiveModel::TestCase topic = Topic.new topic.approved = (base + 1).to_s - assert topic.invalid? + assert_predicate topic, :invalid? end def test_validates_numericality_with_invalid_args diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index 642dd0f144..c3eca41070 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -15,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 @@ -41,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 @@ -49,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 @@ -76,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 011033606e..80c347703a 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/person" require "models/topic" @@ -35,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 @@ -60,56 +62,62 @@ class ValidatesTest < ActiveModel::TestCase end def test_validates_with_if_as_local_conditions - Person.validates :karma, presence: true, email: { unless: :condition_is_true } + Person.validates :karma, presence: true, email: { if: :condition_is_false } person = Person.new person.valid? assert_equal ["can't be blank"], person.errors[:karma] end def test_validates_with_if_as_shared_conditions - Person.validates :karma, presence: true, email: true, if: :condition_is_true + Person.validates :karma, presence: true, email: true, if: :condition_is_false + person = Person.new + assert_predicate person, :valid? + end + + def test_validates_with_unless_as_local_conditions + Person.validates :karma, presence: true, email: { unless: :condition_is_true } person = Person.new person.valid? - assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort + assert_equal ["can't be blank"], person.errors[:karma] end def test_validates_with_unless_shared_conditions 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 @@ -151,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/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb index 25c37a572f..024eb1882f 100644 --- a/activemodel/test/cases/validations/validations_context_test.rb +++ b/activemodel/test/cases/validations/validations_context_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 5ce86738cd..8239792c79 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -63,71 +65,35 @@ 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 - test "with if statements that return false" do - ActiveSupport::Deprecation.silence do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2") - end - topic = Topic.new - assert topic.valid? - end - - test "with if statements that return true" do - ActiveSupport::Deprecation.silence do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1") - end - topic = Topic.new - assert topic.invalid? - assert_includes topic.errors[:base], ERROR_MESSAGE - end - - test "with unless statements that return true" do - ActiveSupport::Deprecation.silence do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1") - end - topic = Topic.new - assert topic.valid? - end - - test "with unless statements that returns false" do - ActiveSupport::Deprecation.silence do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2") - end - topic = Topic.new - assert topic.invalid? - assert_includes topic.errors[:base], ERROR_MESSAGE - end - test "passes all configuration options to the validator class" do topic = Topic.new validator = Minitest::Mock.new - validator.expect(:new, validator, [{ foo: :bar, if: "1 == 1", class: Topic }]) + validator.expect(:new, validator, [{ foo: :bar, if: :condition_is_true, class: Topic }]) validator.expect(:validate, nil, [topic]) validator.expect(:is_a?, false, [Symbol]) validator.expect(:is_a?, false, [String]) - ActiveSupport::Deprecation.silence do - Topic.validates_with(validator, if: "1 == 1", foo: :bar) - end - assert topic.valid? + Topic.validates_with(validator, if: :condition_is_true, foo: :bar) + 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 @@ -147,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 @@ -176,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 6647191205..7776233db5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "models/topic" @@ -28,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 @@ -36,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" @@ -109,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 @@ -120,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] @@ -133,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] @@ -144,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 @@ -212,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 @@ -230,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] @@ -256,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) @@ -268,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 @@ -329,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 @@ -379,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 @@ -412,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 @@ -437,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 @@ -457,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/account.rb b/activemodel/test/models/account.rb index eed668d38f..40408e5708 100644 --- a/activemodel/test/models/account.rb +++ b/activemodel/test/models/account.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Account include ActiveModel::ForbiddenAttributesProtection diff --git a/activemodel/test/models/blog_post.rb b/activemodel/test/models/blog_post.rb index 46eba857df..d4b02eeaa7 100644 --- a/activemodel/test/models/blog_post.rb +++ b/activemodel/test/models/blog_post.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Blog def self.use_relative_model_naming? true diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index 113ab0bc1f..c40a6d6f0e 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Contact extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb index dc26bb10ff..df605e93d9 100644 --- a/activemodel/test/models/custom_reader.rb +++ b/activemodel/test/models/custom_reader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CustomReader include ActiveModel::Validations diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb index 933f3c463a..fe82c463d3 100644 --- a/activemodel/test/models/helicopter.rb +++ b/activemodel/test/models/helicopter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Helicopter include ActiveModel::Conversion end diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb index e896e90f98..8dd8ceadad 100644 --- a/activemodel/test/models/person.rb +++ b/activemodel/test/models/person.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Person include ActiveModel::Validations extend ActiveModel::Translation @@ -7,6 +9,10 @@ class Person def condition_is_true true end + + def condition_is_false + false + end end class Person::Gender diff --git a/activemodel/test/models/person_with_validator.rb b/activemodel/test/models/person_with_validator.rb index 505ed880c1..44e78cbc29 100644 --- a/activemodel/test/models/person_with_validator.rb +++ b/activemodel/test/models/person_with_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonWithValidator include ActiveModel::Validations diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb index 3fe11043d2..6bb18f95fe 100644 --- a/activemodel/test/models/reply.rb +++ b/activemodel/test/models/reply.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "models/topic" class Reply < Topic diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb index 7aba055c4f..30dd9ce192 100644 --- a/activemodel/test/models/sheep.rb +++ b/activemodel/test/models/sheep.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Sheep extend ActiveModel::Naming end diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb index 192786c096..b0af00ee45 100644 --- a/activemodel/test/models/topic.rb +++ b/activemodel/test/models/topic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Topic include ActiveModel::Validations include ActiveModel::Validations::Callbacks @@ -21,7 +23,7 @@ class Topic true end - def condition_is_true_but_its_not + def condition_is_false false end diff --git a/activemodel/test/models/track_back.rb b/activemodel/test/models/track_back.rb index 357ee37d6d..728a022db5 100644 --- a/activemodel/test/models/track_back.rb +++ b/activemodel/test/models/track_back.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Post class TrackBack def to_model diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index 6556b1a7d9..e98fd8a0a1 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User extend ActiveModel::Callbacks include ActiveModel::SecurePassword diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 22ad1a3c3d..9da004ffcc 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Visitor extend ActiveModel::Callbacks include ActiveModel::SecurePassword diff --git a/activemodel/test/validators/email_validator.rb b/activemodel/test/validators/email_validator.rb index f733c45cf0..0c634d8659 100644 --- a/activemodel/test/validators/email_validator.rb +++ b/activemodel/test/validators/email_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << (options[:message] || "is not an email") unless diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb index 8639045b0d..e7815d92dc 100644 --- a/activemodel/test/validators/namespace/email_validator.rb +++ b/activemodel/test/validators/namespace/email_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "validators/email_validator" module Namespace |