Merge pull request #40095 from ChaelCodes/cc-comparablity-validator

Add ComparisonValidator to validate comparison of any objects
This commit is contained in:
Matthew Draper 2021-04-24 13:20:57 +09:30 committed by GitHub
commit 536a2c0011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 477 additions and 89 deletions

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module ActiveModel
module Validations
module Comparability #:nodoc:
COMPARE_CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
other_than: :!= }.freeze
def option_value(record, option_value)
case option_value
when Proc
option_value.call(record)
when Symbol
record.send(option_value)
else
option_value
end
end
def error_options(value, option_value)
options.except(*COMPARE_CHECKS.keys).merge!(
count: option_value,
value: value
)
end
def error_value(record, option_value)
case option_value
when Proc
option_value(record, option_value)
else
option_value
end
end
end
end
end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module ActiveModel
module Validations
class ComparisonValidator < EachValidator # :nodoc:
include Comparability
def check_validity!
unless (options.keys & COMPARE_CHECKS.keys).any?
raise ArgumentError, "Expected one of :greater_than, :greater_than_or_equal_to, "\
":equal_to, :less_than, :less_than_or_equal_to, nor :other_than supplied."
end
end
def validate_each(record, attr_name, value)
options.slice(*COMPARE_CHECKS.keys).each do |option, raw_option_value|
if value.nil? || value.blank?
return record.errors.add(attr_name, :blank, **error_options(value, error_value(record, raw_option_value)))
end
unless value.send(COMPARE_CHECKS[option], option_value(record, raw_option_value))
record.errors.add(attr_name, option, **error_options(value, error_value(record, raw_option_value)))
end
rescue ArgumentError => e
record.errors.add(attr_name, e.message)
end
end
end
module HelperMethods
# Validates the value of a specified attribute fulfills all
# defined comparisons with another value, proc, or attribute.
#
# class Person < ActiveRecord::Base
# validates_comparison_of :value, greater_than: 'the sum of its parts'
# end
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "failed comparison").
# * <tt>:greater_than</tt> - Specifies the value must be greater than the
# supplied value.
# * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
# greater than or equal the supplied value.
# * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
# value.
# * <tt>:less_than</tt> - Specifies the value must be less than the
# supplied value.
# * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
# than or equal the supplied value.
# * <tt>:other_than</tt> - Specifies the value must not be equal to the
# supplied value.
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
# See <tt>ActiveModel::Validations#validates</tt> for more information
#
# The validator requires at least one of the following checks be supplied.
# Each will accept a proc, value, or a symbol which corresponds to a method:
#
# * <tt>:greater_than</tt>
# * <tt>:greater_than_or_equal_to</tt>
# * <tt>:equal_to</tt>
# * <tt>:less_than</tt>
# * <tt>:less_than_or_equal_to</tt>
# * <tt>:other_than</tt>
#
# For example:
#
# class Person < ActiveRecord::Base
# validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
# validates_comparison_of :preferred_name, other_than: :given_name, allow_nil: true
# end
def validates_comparison_of(*attr_names)
validates_with ComparisonValidator, _merge_attributes(attr_names)
end
end
end
end

View File

@ -5,24 +5,25 @@ require "bigdecimal/util"
module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
odd: :odd?, even: :even?, other_than: :!=, in: :in? }.freeze
include Comparability
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
RANGE_CHECKS = { in: :in? }
NUMBER_CHECKS = { odd: :odd?, even: :even? }
RESERVED_OPTIONS = COMPARE_CHECKS.keys + NUMBER_CHECKS.keys + RANGE_CHECKS.keys + [:only_integer]
INTEGER_REGEX = /\A[+-]?\d+\z/
HEXADECIMAL_REGEX = /\A[+-]?0[xX]/
def check_validity!
keys = CHECKS.keys - [:odd, :even, :in]
options.slice(*keys).each do |option, value|
options.slice(*COMPARE_CHECKS.keys).each do |option, value|
unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
end
end
options.slice(:in).each do |option, value|
options.slice(*RANGE_CHECKS).each do |option, value|
unless value.is_a?(Range)
raise ArgumentError, ":#{option} must be a range"
end
@ -42,23 +43,18 @@ module ActiveModel
value = parse_as_number(value, precision, scale)
options.slice(*CHECKS.keys).each do |option, option_value|
case option
when :odd, :even
unless value.to_i.public_send(CHECKS[option])
options.slice(*RESERVED_OPTIONS).each do |option, option_value|
if NUMBER_CHECKS.keys.include? option
unless value.to_i.send(NUMBER_CHECKS[option])
record.errors.add(attr_name, option, **filtered_options(value))
end
else
case option_value
when Proc
option_value = option_value.call(record)
when Symbol
option_value = record.send(option_value)
elsif RANGE_CHECKS.keys.include? option
unless value.send(RANGE_CHECKS[option], option_value)
record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
end
option_value = parse_as_number(option_value, precision, scale, option)
unless value.public_send(CHECKS[option], option_value)
elsif COMPARE_CHECKS.keys.include? option
option_value = option_as_number(record, option_value, precision, scale)
unless value.send(COMPARE_CHECKS[option], option_value)
record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
end
end
@ -66,10 +62,12 @@ module ActiveModel
end
private
def parse_as_number(raw_value, precision, scale, option = nil)
if option == :in
raw_value if raw_value.is_a?(Range)
elsif raw_value.is_a?(Float)
def option_as_number(record, option_value, precision, scale)
parse_as_number(option_value(record, option_value), precision, scale)
end
def parse_as_number(raw_value, precision, scale)
if raw_value.is_a?(Float)
parse_float(raw_value, precision, scale)
elsif raw_value.is_a?(BigDecimal)
round(raw_value, scale)
@ -180,6 +178,7 @@ module ActiveModel
# supplied value.
# * <tt>:odd</tt> - Specifies the value must be an odd number.
# * <tt>:even</tt> - Specifies the value must be an even number.
# * <tt>:in</tt> - Check that the value is within a range.
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .

View File

@ -0,0 +1,243 @@
# frozen_string_literal: true
require "cases/helper"
require "models/topic"
require "models/person"
class ComparisonValidationTest < ActiveModel::TestCase
def teardown
Topic.clear_validators!
end
def test_validates_comparison_with_greater_than_using_numeric
Topic.validates_comparison_of :approved, greater_than: 10
assert_invalid_values([-12, 10], "must be greater than 10")
assert_valid_values([11])
end
def test_validates_comparison_with_greater_than_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, greater_than: date_value
assert_invalid_values([
Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
Date.parse("2020-08-02"),
DateTime.new(2020, 8, 1, 12, 34)], "must be greater than 2020-08-02")
assert_valid_values([Date.parse("2020-08-03"), DateTime.new(2020, 8, 2, 12, 34)])
end
def test_validates_comparison_with_greater_than_using_string
Topic.validates_comparison_of :approved, greater_than: "cat"
assert_invalid_values(["ant", "cat"], "must be greater than cat")
assert_valid_values(["dog", "whale"])
end
def test_validates_comparison_with_greater_than_or_equal_to_using_numeric
Topic.validates_comparison_of :approved, greater_than_or_equal_to: 10
assert_invalid_values([-12, 5], "must be greater than or equal to 10")
assert_valid_values([11, 10])
end
def test_validates_comparison_with_greater_than_or_equal_to_using_string
Topic.validates_comparison_of :approved, greater_than_or_equal_to: "cat"
assert_invalid_values(["ant"], "must be greater than or equal to cat")
assert_valid_values(["cat", "dog", "whale"])
end
def test_validates_comparison_with_greater_than_or_equal_to_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, greater_than_or_equal_to: date_value
assert_invalid_values([
Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
DateTime.new(2020, 8, 1, 12, 34)], "must be greater than or equal to 2020-08-02")
assert_valid_values([Date.parse("2020-08-03"), DateTime.new(2020, 8, 2, 12, 34), Date.parse("2020-08-02")])
end
def test_validates_comparison_with_equal_to_using_numeric
Topic.validates_comparison_of :approved, equal_to: 10
assert_invalid_values([-12, 5, 11], "must be equal to 10")
assert_valid_values([10])
end
def test_validates_comparison_with_equal_to_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, equal_to: date_value
assert_invalid_values([
Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
DateTime.new(2020, 8, 1, 12, 34),
Date.parse("2020-08-03"),
DateTime.new(2020, 8, 2, 12, 34)], "must be equal to 2020-08-02")
assert_valid_values([Date.parse("2020-08-02"), DateTime.new(2020, 8, 2, 0, 0)])
end
def test_validates_comparison_with_less_than_using_numeric
Topic.validates_comparison_of :approved, less_than: 10
assert_invalid_values([11, 10], "must be less than 10")
assert_valid_values([-12, -5, 5])
end
def test_validates_comparison_with_less_than_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, less_than: date_value
assert_invalid_values([
Date.parse("2020-08-02"),
Date.parse("2020-08-03"),
DateTime.new(2020, 8, 2, 12, 34)], "must be less than 2020-08-02")
assert_valid_values([Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
DateTime.new(2020, 8, 1, 12, 34)])
end
def test_validates_comparison_with_less_than_or_equal_to_using_numeric
Topic.validates_comparison_of :approved, less_than_or_equal_to: 10
assert_invalid_values([12], "must be less than or equal to 10")
assert_valid_values([-11, 5, 10])
end
def test_validates_comparison_with_less_than_or_equal_to_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, less_than_or_equal_to: date_value
assert_invalid_values([
Date.parse("2020-08-03"),
DateTime.new(2020, 8, 2, 12, 34)], "must be less than or equal to 2020-08-02")
assert_valid_values([Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
Date.parse("2020-08-02"),
DateTime.new(2020, 8, 1, 12, 34)])
end
def test_validates_comparison_with_other_than_using_numeric
Topic.validates_comparison_of :approved, other_than: 10
assert_invalid_values([10], "must be other than 10")
assert_valid_values([-12, 5, 11])
end
def test_validates_comparison_with_other_than_using_date
date_value = Date.parse("2020-08-02")
Topic.validates_comparison_of :approved, other_than: date_value
assert_invalid_values([Date.parse("2020-08-02"), DateTime.new(2020, 8, 2, 0, 0)], "must be other than 2020-08-02")
assert_valid_values([
Date.parse("2019-08-03"),
Date.parse("2020-07-03"),
Date.parse("2020-08-01"),
DateTime.new(2020, 8, 1, 12, 34),
Date.parse("2020-08-03"),
DateTime.new(2020, 8, 2, 12, 34)])
end
def test_validates_comparison_with_proc
Topic.define_method(:requested) { Date.new(2020, 8, 1) }
Topic.validates_comparison_of :approved, greater_than_or_equal_to: Proc.new(&:requested)
assert_invalid_values([Date.new(2020, 7, 1), Date.new(2019, 7, 1), DateTime.new(2020, 7, 1, 22, 34)])
assert_valid_values([Date.new(2020, 8, 2), DateTime.new(2021, 8, 1)])
ensure
Topic.remove_method :requested
end
def test_validates_comparison_with_method
Topic.define_method(:requested) { Date.new(2020, 8, 1) }
Topic.validates_comparison_of :approved, greater_than_or_equal_to: :requested
assert_invalid_values([Date.new(2020, 7, 1), Date.new(2019, 7, 1), DateTime.new(2020, 7, 1, 22, 34)])
assert_valid_values([Date.new(2020, 8, 2), DateTime.new(2021, 8, 1)])
ensure
Topic.remove_method :requested
end
def test_validates_comparison_with_custom_compare
custom = Struct.new(:amount) {
include Comparable
def <=>(other)
amount % 100 <=> other.amount % 100
end
}
Topic.validates_comparison_of :approved, greater_than_or_equal_to: custom.new(1150)
assert_invalid_values([custom.new(530), custom.new(2325)])
assert_valid_values([custom.new(575), custom.new(250), custom.new(1999)])
end
def test_validates_comparison_with_blank_allowed
Topic.validates_comparison_of :approved, greater_than: "cat", allow_blank: true
assert_invalid_values(["ant"])
assert_valid_values([nil, ""])
end
def test_validates_comparison_with_nil_allowed
Topic.validates_comparison_of :approved, less_than: 100, allow_nil: true
assert_invalid_values([200])
assert_valid_values([nil, 50])
end
def test_validates_comparison_of_incomparables
Topic.validates_comparison_of :approved, less_than: "cat"
assert_invalid_values([12], "comparison of Integer with String failed")
assert_invalid_values([nil])
assert_valid_values([])
end
def test_validates_comparison_of_multiple_values
Topic.validates_comparison_of :approved, other_than: 17, greater_than: 13
assert_invalid_values([12, nil, 17])
assert_valid_values([15])
end
def test_validates_comparison_of_no_options
error = assert_raises(ArgumentError) do
Topic.validates_comparison_of(:approved)
end
assert_equal "Expected one of :greater_than, :greater_than_or_equal_to, :equal_to," \
" :less_than, :less_than_or_equal_to, nor :other_than supplied.", error.message
end
private
def assert_invalid_values(values, error = nil)
with_each_topic_approved_value(values) do |topic, value|
assert topic.invalid?, "#{value.inspect} failed comparison"
assert topic.errors[:approved].any?, "FAILED for #{value.inspect}"
assert_equal error, topic.errors[:approved].first if error
end
end
def assert_valid_values(values)
with_each_topic_approved_value(values) do |topic, value|
assert topic.valid?, "#{value.inspect} failed comparison with validation error: #{topic.errors[:approved].first}"
end
end
def with_each_topic_approved_value(values)
topic = Topic.new(title: "comparison test", content: "whatever")
values.each do |value|
topic.approved = value
yield topic, value
end
end
end

View File

@ -27,206 +27,206 @@ class NumericalityValidationTest < ActiveModel::TestCase
def test_default_validates_numericality_of
Topic.validates_numericality_of :approved
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
assert_invalid_values(NIL + BLANK + JUNK)
assert_valid_values(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_nil_allowed
Topic.validates_numericality_of :approved, allow_nil: true
invalid!(JUNK + BLANK)
valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
assert_invalid_values(JUNK + BLANK)
assert_valid_values(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_blank_allowed
Topic.validates_numericality_of :approved, allow_blank: true
invalid!(JUNK)
valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
assert_invalid_values(JUNK)
assert_valid_values(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only
Topic.validates_numericality_of :approved, only_integer: true
invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
valid!(INTEGERS)
assert_invalid_values(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
assert_valid_values(INTEGERS)
end
def test_validates_numericality_of_with_integer_only_and_nil_allowed
Topic.validates_numericality_of :approved, only_integer: true, allow_nil: true
invalid!(JUNK + BLANK + FLOATS + BIGDECIMAL + INFINITY)
valid!(NIL + INTEGERS)
assert_invalid_values(JUNK + BLANK + FLOATS + BIGDECIMAL + INFINITY)
assert_valid_values(NIL + INTEGERS)
end
def test_validates_numericality_of_with_integer_only_and_symbol_as_value
Topic.validates_numericality_of :approved, only_integer: :condition_is_false
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
assert_invalid_values(NIL + BLANK + JUNK)
assert_valid_values(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only_and_proc_as_value
Topic.define_method(:allow_only_integers?) { false }
Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?)
invalid!(NIL + BLANK + JUNK)
valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
assert_invalid_values(NIL + BLANK + JUNK)
assert_valid_values(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_with_greater_than
Topic.validates_numericality_of :approved, greater_than: 10
invalid!([-10, 10], "must be greater than 10")
valid!([11])
assert_invalid_values([-10, 10], "must be greater than 10")
assert_valid_values([11])
end
def test_validates_numericality_with_greater_than_using_differing_numeric_types
Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18")
invalid!([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
valid!([97.19, 98, BigDecimal("98"), BigDecimal("97.19")])
assert_invalid_values([-97.18, BigDecimal("97.18"), BigDecimal("-97.18")], "must be greater than 97.18")
assert_valid_values([97.19, 98, BigDecimal("98"), BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_using_string_value
Topic.validates_numericality_of :approved, greater_than: 10
invalid!(["-10", "9", "9.9", "10"], "must be greater than 10")
valid!(["10.1", "11"])
assert_invalid_values(["-10", "9", "9.9", "10"], "must be greater than 10")
assert_valid_values(["10.1", "11"])
end
def test_validates_numericality_with_greater_than_or_equal
Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
invalid!([-9, 9], "must be greater than or equal to 10")
valid!([10])
assert_invalid_values([-9, 9], "must be greater than or equal to 10")
assert_valid_values([10])
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("97.18")
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")])
assert_invalid_values([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18")
assert_valid_values([97.18, 98, BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_or_equal_using_string_value
Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
invalid!(["-10", "9", "9.9"], "must be greater than or equal to 10")
valid!(["10", "10.1", "11"])
assert_invalid_values(["-10", "9", "9.9"], "must be greater than or equal to 10")
assert_valid_values(["10", "10.1", "11"])
end
def test_validates_numericality_with_equal_to
Topic.validates_numericality_of :approved, equal_to: 10
invalid!([-10, 11] + INFINITY, "must be equal to 10")
valid!([10])
assert_invalid_values([-10, 11] + INFINITY, "must be equal to 10")
assert_valid_values([10])
end
def test_validates_numericality_with_equal_to_using_differing_numeric_types
Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18")
invalid!([-97.18], "must be equal to 97.18")
valid!([BigDecimal("97.18")])
assert_invalid_values([-97.18], "must be equal to 97.18")
assert_valid_values([BigDecimal("97.18")])
end
def test_validates_numericality_with_equal_to_using_string_value
Topic.validates_numericality_of :approved, equal_to: 10
invalid!(["-10", "9", "9.9", "10.1", "11"], "must be equal to 10")
valid!(["10"])
assert_invalid_values(["-10", "9", "9.9", "10.1", "11"], "must be equal to 10")
assert_valid_values(["10"])
end
def test_validates_numericality_with_less_than
Topic.validates_numericality_of :approved, less_than: 10
invalid!([10], "must be less than 10")
valid!([-9, 9])
assert_invalid_values([10], "must be less than 10")
assert_valid_values([-9, 9])
end
def test_validates_numericality_with_less_than_using_differing_numeric_types
Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18")
invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18")
valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
assert_invalid_values([97.18, BigDecimal("97.18")], "must be less than 97.18")
assert_valid_values([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
end
def test_validates_numericality_with_less_than_using_string_value
Topic.validates_numericality_of :approved, less_than: 10
invalid!(["10", "10.1", "11"], "must be less than 10")
valid!(["-10", "9", "9.9"])
assert_invalid_values(["10", "10.1", "11"], "must be less than 10")
assert_valid_values(["-10", "9", "9.9"])
end
def test_validates_numericality_with_less_than_or_equal_to
Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
invalid!([11], "must be less than or equal to 10")
valid!([-10, 10])
assert_invalid_values([11], "must be less than or equal to 10")
assert_valid_values([-10, 10])
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("97.18")
invalid!([97.19, 98], "must be less than or equal to 97.18")
valid!([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")])
assert_invalid_values([97.19, 98], "must be less than or equal to 97.18")
assert_valid_values([-97.18, BigDecimal("-97.18"), BigDecimal("97.18")])
end
def test_validates_numericality_with_less_than_or_equal_using_string_value
Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
invalid!(["10.1", "11"], "must be less than or equal to 10")
valid!(["-10", "9", "9.9", "10"])
assert_invalid_values(["10.1", "11"], "must be less than or equal to 10")
assert_valid_values(["-10", "9", "9.9", "10"])
end
def test_validates_numericality_with_odd
Topic.validates_numericality_of :approved, odd: true
invalid!([-2, 2], "must be odd")
valid!([-1, 1])
assert_invalid_values([-2, 2], "must be odd")
assert_valid_values([-1, 1])
end
def test_validates_numericality_with_even
Topic.validates_numericality_of :approved, even: true
invalid!([-1, 1], "must be even")
valid!([-2, 2])
assert_invalid_values([-1, 1], "must be even")
assert_valid_values([-2, 2])
end
def test_validates_numericality_with_greater_than_less_than_and_even
Topic.validates_numericality_of :approved, greater_than: 1, less_than: 4, even: true
invalid!([1, 3, 4])
valid!([2])
assert_invalid_values([1, 3, 4])
assert_valid_values([2])
end
def test_validates_numericality_with_other_than
Topic.validates_numericality_of :approved, other_than: 0
invalid!([0, 0.0])
valid!([-1, 42])
assert_invalid_values([0, 0.0])
assert_valid_values([-1, 42])
end
def test_validates_numericality_with_in
Topic.validates_numericality_of :approved, in: 1..3
invalid!([0, 4])
valid!([1, 2, 3])
assert_invalid_values([0, 4])
assert_valid_values([1, 2, 3])
end
def test_validates_numericality_with_other_than_using_string_value
Topic.validates_numericality_of :approved, other_than: 0
invalid!(["0", "0.0"])
valid!(["-1", "1.1", "42"])
assert_invalid_values(["0", "0.0"])
assert_valid_values(["-1", "1.1", "42"])
end
def test_validates_numericality_with_proc
Topic.define_method(:min_approved) { 5 }
Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved)
invalid!([3, 4])
valid!([5, 6])
assert_invalid_values([3, 4])
assert_valid_values([5, 6])
ensure
Topic.remove_method :min_approved
end
@ -235,8 +235,8 @@ class NumericalityValidationTest < ActiveModel::TestCase
Topic.define_method(:max_approved) { 5 }
Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved
invalid!([6])
valid!([4, 5])
assert_invalid_values([6])
assert_valid_values([4, 5])
ensure
Topic.remove_method :max_approved
end
@ -313,12 +313,12 @@ class NumericalityValidationTest < ActiveModel::TestCase
def test_validates_numericality_equality_for_float_and_big_decimal
Topic.validates_numericality_of :approved, equal_to: BigDecimal("65.6")
invalid!([Float("65.5"), BigDecimal("65.7")], "must be equal to 65.6")
valid!([Float("65.6"), BigDecimal("65.6")])
assert_invalid_values([Float("65.5"), BigDecimal("65.7")], "must be equal to 65.6")
assert_valid_values([Float("65.6"), BigDecimal("65.6")])
end
private
def invalid!(values, error = nil)
def assert_invalid_values(values, error = nil)
with_each_topic_approved_value(values) do |topic, value|
assert topic.invalid?, "#{value.inspect} not rejected as a number"
assert topic.errors[:approved].any?, "FAILED for #{value.inspect}"
@ -326,7 +326,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
end
def valid!(values)
def assert_valid_values(values)
with_each_topic_approved_value(values) do |topic, value|
assert topic.valid?, "#{value.inspect} not accepted as a number with validation error: #{topic.errors[:approved].first}"
end

View File

@ -387,6 +387,36 @@ end
The default error message for this helper is _"doesn't match confirmation"_.
### `comparison`
This check will validate a comparison between any two comparable values.
The validator requires a compare option be supplied. Each option accepts a
value, proc, or symbol. Any class that includes Comparable can be compared.
```ruby
class Promotion < ApplicationRecord
validates :start_date, comparison: { greater_than: :end_date }
end
```
These options are all supported:
* `:greater_than` - Specifies the value must be greater than the supplied
value. The default error message for this option is _"must be greater than
%{count}"_.
* `:greater_than_or_equal_to` - Specifies the value must be greater than or
equal to the supplied value. The default error message for this option is
_"must be greater than or equal to %{count}"_.
* `:equal_to` - Specifies the value must be equal to the supplied value. The
default error message for this option is _"must be equal to %{count}"_.
* `:less_than` - Specifies the value must be less than the supplied value. The
default error message for this option is _"must be less than %{count}"_.
* `:less_than_or_equal_to` - Specifies the value must be less than or equal to
the supplied value. The default error message for this option is _"must be
less than or equal to %{count}"_.
* `:other_than` - Specifies the value must be other than the supplied value.
The default error message for this option is _"must be other than %{count}"_.
### `exclusion`
This helper validates that the attributes' values are not included in a given