Fix NumericalityValidator on object responding to to_f:

- If you had a PORO that acted like a Numeric, the validator would
  work correctly because it was previously using `Kernel.Float`
  which is implicitely calling `to_f` on the passed argument.

  Since rails/rails@d126c0d , we are now using `BigDecimal` which does
  not implicitely call `to_f` on the argument, making the validator
  fail with an underlying `TypeError` exception.

  This patch replate the `is_decimal?` check with `Kernel.Float`.
  Using `Kernel.Float` as argument for the BigDecimal call has two
  advantages:

  1. It calls `to_f` implicetely for us.
  2. It's also smart enough to detect that `Kernel.Float("a")` isn't a
     Numeric and will raise an error.
     We don't need the `is_decimal?` check thanks to that.

  Passing `Float::DIG` as second argument to `BigDecimal` is mandatory
  because the precision can't be omitted when passing a Float.
  `Float::DIG` is what is used internally by ruby when calling
  `123.to_d`

  https://github.com/ruby/ruby/blob/trunk/ext/bigdecimal/lib/bigdecimal/util.rb#L47

- Another small issue introduced in https://github.com/rails/rails/pull/34693
  would now raise a TypeError because `Regexp#===` will just return
  false if the passed argument isn't a string or symbol, whereas
  `Regexp#match?` will.
This commit is contained in:
Edouard CHIN 2019-01-22 00:18:56 +01:00
parent 4f62e757ca
commit f01e38509c
2 changed files with 16 additions and 8 deletions

@ -10,7 +10,6 @@ class NumericalityValidator < EachValidator # :nodoc:
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
INTEGER_REGEX = /\A[+-]?\d+\z/
DECIMAL_REGEX = /\A[+-]?\d+\.?\d*(e|e[+-])?\d+\z/
def check_validity!
keys = CHECKS.keys - [:odd, :even]
@ -92,8 +91,8 @@ def parse_as_number(raw_value)
raw_value
elsif is_integer?(raw_value)
raw_value.to_i
elsif is_decimal?(raw_value) && !is_hexadecimal_literal?(raw_value)
BigDecimal(raw_value)
elsif !is_hexadecimal_literal?(raw_value)
Kernel.Float(raw_value).to_d
end
end
@ -101,12 +100,8 @@ def is_integer?(raw_value)
INTEGER_REGEX.match?(raw_value.to_s)
end
def is_decimal?(raw_value)
DECIMAL_REGEX.match?(raw_value.to_s)
end
def is_hexadecimal_literal?(raw_value)
/\A0[xX]/.match?(raw_value)
/\A0[xX]/.match?(raw_value.to_s)
end
def filtered_options(value)

@ -281,6 +281,19 @@ def test_validates_numericality_with_exponent_number
assert_predicate topic, :invalid?
end
def test_validates_numericalty_with_object_acting_as_numeric
klass = Class.new do
def to_f
123.54
end
end
Topic.validates_numericality_of :price
topic = Topic.new(price: klass.new)
assert_predicate topic, :valid?
end
def test_validates_numericality_with_invalid_args
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" }
assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" }