activemodel 7.0.5 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -123
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +11 -11
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +26 -1
  9. data/lib/active_model/attribute_assignment.rb +1 -1
  10. data/lib/active_model/attribute_methods.rb +103 -64
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +10 -1
  13. data/lib/active_model/attributes.rb +62 -45
  14. data/lib/active_model/callbacks.rb +5 -5
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +5 -4
  19. data/lib/active_model/errors.rb +37 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +4 -4
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +34 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +61 -23
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +10 -2
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/mutable.rb +4 -4
  40. data/lib/active_model/type/helpers/numeric.rb +6 -1
  41. data/lib/active_model/type/helpers/time_value.rb +28 -12
  42. data/lib/active_model/type/immutable_string.rb +37 -1
  43. data/lib/active_model/type/integer.rb +44 -1
  44. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  45. data/lib/active_model/type/string.rb +9 -1
  46. data/lib/active_model/type/time.rb +48 -7
  47. data/lib/active_model/type/value.rb +23 -3
  48. data/lib/active_model/type.rb +1 -0
  49. data/lib/active_model/validations/absence.rb +1 -1
  50. data/lib/active_model/validations/acceptance.rb +1 -1
  51. data/lib/active_model/validations/callbacks.rb +4 -4
  52. data/lib/active_model/validations/clusivity.rb +5 -8
  53. data/lib/active_model/validations/comparability.rb +0 -11
  54. data/lib/active_model/validations/comparison.rb +15 -7
  55. data/lib/active_model/validations/format.rb +6 -7
  56. data/lib/active_model/validations/length.rb +10 -8
  57. data/lib/active_model/validations/numericality.rb +35 -23
  58. data/lib/active_model/validations/presence.rb +1 -1
  59. data/lib/active_model/validations/resolve_value.rb +26 -0
  60. data/lib/active_model/validations/validates.rb +4 -4
  61. data/lib/active_model/validations/with.rb +9 -2
  62. data/lib/active_model/validations.rb +44 -9
  63. data/lib/active_model/validator.rb +7 -5
  64. data/lib/active_model/version.rb +1 -1
  65. data/lib/active_model.rb +5 -1
  66. metadata +16 -11
@@ -2,7 +2,39 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class ImmutableString < Value # :nodoc:
5
+ # = Active Model \ImmutableString \Type
6
+ #
7
+ # Attribute type to represent immutable strings. It casts incoming values to
8
+ # frozen strings.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :name, :immutable_string
14
+ # end
15
+ #
16
+ # person = Person.new
17
+ # person.name = 1
18
+ #
19
+ # person.name # => "1"
20
+ # person.name.frozen? # => true
21
+ #
22
+ # Values are coerced to strings using their +to_s+ method. Boolean values
23
+ # are treated differently, however: +true+ will be cast to <tt>"t"</tt> and
24
+ # +false+ will be cast to <tt>"f"</tt>. These strings can be customized when
25
+ # declaring an attribute:
26
+ #
27
+ # class Person
28
+ # include ActiveModel::Attributes
29
+ #
30
+ # attribute :active, :immutable_string, true: "aye", false: "nay"
31
+ # end
32
+ #
33
+ # person = Person.new
34
+ # person.active = true
35
+ #
36
+ # person.active # => "aye"
37
+ class ImmutableString < Value
6
38
  def initialize(**args)
7
39
  @true = -(args.delete(:true)&.to_s || "t")
8
40
  @false = -(args.delete(:false)&.to_s || "f")
@@ -22,6 +54,10 @@ module ActiveModel
22
54
  end
23
55
  end
24
56
 
57
+ def serialize_cast_value(value) # :nodoc:
58
+ value
59
+ end
60
+
25
61
  private
26
62
  def cast_value(value)
27
63
  case value
@@ -2,7 +2,46 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class Integer < Value # :nodoc:
5
+ # = Active Model \Integer \Type
6
+ #
7
+ # Attribute type for integer representation. This type is registered under
8
+ # the +:integer+ key.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :age, :integer
14
+ # end
15
+ #
16
+ # Values are cast using their +to_i+ method, except for blank strings, which
17
+ # are cast to +nil+. If a +to_i+ method is not defined or raises an error,
18
+ # the value will be cast to +nil+.
19
+ #
20
+ # person = Person.new
21
+ #
22
+ # person.age = "18"
23
+ # person.age # => 18
24
+ #
25
+ # person.age = ""
26
+ # person.age # => nil
27
+ #
28
+ # person.age = :not_an_integer
29
+ # person.age # => nil (because Symbol does not define #to_i)
30
+ #
31
+ # Serialization also works under the same principle. Non-numeric strings are
32
+ # serialized as +nil+, for example.
33
+ #
34
+ # Serialization also validates that the integer can be stored using a
35
+ # limited number of bytes. If it cannot, an ActiveModel::RangeError will be
36
+ # raised. The default limit is 4 bytes, and can be customized when declaring
37
+ # an attribute:
38
+ #
39
+ # class Person
40
+ # include ActiveModel::Attributes
41
+ #
42
+ # attribute :age, :integer, limit: 6
43
+ # end
44
+ class Integer < Value
6
45
  include Helpers::Numeric
7
46
 
8
47
  # Column storage size in bytes.
@@ -28,6 +67,10 @@ module ActiveModel
28
67
  ensure_in_range(super)
29
68
  end
30
69
 
70
+ def serialize_cast_value(value) # :nodoc:
71
+ ensure_in_range(value)
72
+ end
73
+
31
74
  def serializable?(value)
32
75
  cast_value = cast(value)
33
76
  in_range?(cast_value) || begin
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module SerializeCastValue # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def serialize_cast_value_compatible?
10
+ return @serialize_cast_value_compatible if defined?(@serialize_cast_value_compatible)
11
+ @serialize_cast_value_compatible = ancestors.index(instance_method(:serialize_cast_value).owner) <= ancestors.index(instance_method(:serialize).owner)
12
+ end
13
+ end
14
+
15
+ module DefaultImplementation
16
+ def serialize_cast_value(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ def self.included(klass)
22
+ klass.include DefaultImplementation unless klass.method_defined?(:serialize_cast_value)
23
+ end
24
+
25
+ def self.serialize(type, value)
26
+ # Using `type.equal?(type.itself_if_...)` is a performant way to also
27
+ # ensure that `type` is not just a DelegateClass instance (e.g.
28
+ # ActiveRecord::Type::Serialized) unintentionally delegating
29
+ # SerializeCastValue methods.
30
+ if type.equal?((type.itself_if_serialize_cast_value_compatible rescue nil))
31
+ type.serialize_cast_value(value)
32
+ else
33
+ type.serialize(value)
34
+ end
35
+ end
36
+
37
+ def itself_if_serialize_cast_value_compatible
38
+ self if self.class.serialize_cast_value_compatible?
39
+ end
40
+
41
+ def initialize(...)
42
+ super
43
+ self.class.serialize_cast_value_compatible? # eagerly compute
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,7 +4,15 @@ require "active_model/type/immutable_string"
4
4
 
5
5
  module ActiveModel
6
6
  module Type
7
- class String < ImmutableString # :nodoc:
7
+ # = Active Model \String \Type
8
+ #
9
+ # Attribute type for strings. It is registered under the +:string+ key.
10
+ #
11
+ # This class is a specialization of ActiveModel::Type::ImmutableString. It
12
+ # performs coercion in the same way, and can be configured in the same way.
13
+ # However, it accounts for mutable strings, so dirty tracking can properly
14
+ # check if a string has changed.
15
+ class String < ImmutableString
8
16
  def changed_in_place?(raw_old_value, new_value)
9
17
  if new_value.is_a?(::String)
10
18
  raw_old_value != new_value
@@ -2,12 +2,45 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class Time < Value # :nodoc:
5
+ # = Active Model \Time \Type
6
+ #
7
+ # Attribute type for time of day representation. It is registered under the
8
+ # +:time+ key.
9
+ #
10
+ # class Event
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :start, :time
14
+ # end
15
+ #
16
+ # String values are parsed using the ISO 8601 datetime format, but are
17
+ # normalized to have a date of 2000-01-01 and be in the UTC time zone.
18
+ #
19
+ # event = Event.new
20
+ # event.start = "2004-10-25T01:23:45-06:00"
21
+ #
22
+ # event.start.class # => Time
23
+ # event.start # => 2000-01-01 07:23:45 UTC
24
+ #
25
+ # Partial time-only formats are also accepted.
26
+ #
27
+ # event.start = "00:01:02+03:00"
28
+ # event.start # => 1999-12-31 21:01:02 UTC
29
+ #
30
+ # The degree of sub-second precision can be customized when declaring an
31
+ # attribute:
32
+ #
33
+ # class Event
34
+ # include ActiveModel::Attributes
35
+ #
36
+ # attribute :start, :time, precision: 4
37
+ # end
38
+ class Time < Value
6
39
  include Helpers::Timezone
7
- include Helpers::TimeValue
8
40
  include Helpers::AcceptsMultiparameterTime.new(
9
41
  defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
10
42
  )
43
+ include Helpers::TimeValue
11
44
 
12
45
  def type
13
46
  :time
@@ -19,8 +52,12 @@ module ActiveModel
19
52
  case value
20
53
  when ::String
21
54
  value = "2000-01-01 #{value}"
22
- time_hash = ::Date._parse(value)
23
- return if time_hash[:hour].nil?
55
+ time_hash = begin
56
+ ::Date._parse(value)
57
+ rescue ArgumentError
58
+ end
59
+
60
+ return if time_hash.nil? || time_hash[:hour].nil?
24
61
  when ::Time
25
62
  value = value.change(year: 2000, day: 1, month: 1)
26
63
  end
@@ -31,13 +68,17 @@ module ActiveModel
31
68
  private
32
69
  def cast_value(value)
33
70
  return apply_seconds_precision(value) unless value.is_a?(::String)
34
- return if value.empty?
71
+ return if value.blank?
35
72
 
36
73
  dummy_time_value = value.sub(/\A\d{4}-\d\d-\d\d(?:T|\s)|/, "2000-01-01 ")
37
74
 
38
75
  fast_string_to_time(dummy_time_value) || begin
39
- time_hash = ::Date._parse(dummy_time_value)
40
- return if time_hash[:hour].nil?
76
+ time_hash = begin
77
+ ::Date._parse(dummy_time_value)
78
+ rescue ArgumentError
79
+ end
80
+
81
+ return if time_hash.nil? || time_hash[:hour].nil?
41
82
  new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
42
83
  end
43
84
  end
@@ -2,10 +2,20 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
+ # = Active Model \Value \Type
6
+ #
7
+ # The base class for all attribute types. This class also serves as the
8
+ # default type for attributes that do not specify a type.
5
9
  class Value
10
+ include SerializeCastValue
6
11
  attr_reader :precision, :scale, :limit
7
12
 
13
+ # Initializes a type with three basic configuration settings: precision,
14
+ # limit, and scale. The Value base class does not define behavior for
15
+ # these settings. It uses them for equality comparison and hash key
16
+ # generation only.
8
17
  def initialize(precision: nil, limit: nil, scale: nil)
18
+ super()
9
19
  @precision = precision
10
20
  @scale = scale
11
21
  @limit = limit
@@ -19,7 +29,9 @@ module ActiveModel
19
29
  true
20
30
  end
21
31
 
22
- def type # :nodoc:
32
+ # Returns the unique type name as a Symbol. Subclasses should override
33
+ # this method.
34
+ def type
23
35
  end
24
36
 
25
37
  # Converts a value from database input to the appropriate ruby type. The
@@ -121,8 +133,16 @@ module ActiveModel
121
133
  def assert_valid_value(_)
122
134
  end
123
135
 
124
- def immutable_value(value) # :nodoc:
125
- value
136
+ def serialized? # :nodoc:
137
+ false
138
+ end
139
+
140
+ def mutable? # :nodoc:
141
+ false
142
+ end
143
+
144
+ def as_json(*)
145
+ raise NoMethodError
126
146
  end
127
147
 
128
148
  private
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/type/helpers"
4
+ require "active_model/type/serialize_cast_value"
4
5
  require "active_model/type/value"
5
6
 
6
7
  require "active_model/type/big_integer"
@@ -11,7 +11,7 @@ module ActiveModel
11
11
 
12
12
  module HelperMethods
13
13
  # Validates that the specified attributes are blank (as defined by
14
- # Object#present?). Happens by default on save.
14
+ # Object#present?).
15
15
  #
16
16
  # class Person < ActiveRecord::Base
17
17
  # validates_absence_of :first_name
@@ -90,7 +90,7 @@ module ActiveModel
90
90
  #
91
91
  # If the database column does not exist, the +terms_of_service+ attribute
92
92
  # is entirely virtual. This check is performed only if +terms_of_service+
93
- # is not +nil+ and by default on save.
93
+ # is not +nil+.
94
94
  #
95
95
  # Configuration options:
96
96
  # * <tt>:message</tt> - A custom error message (default is: "must be
@@ -2,12 +2,12 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Validations
5
- # == Active \Model \Validation \Callbacks
5
+ # = Active \Model \Validation \Callbacks
6
6
  #
7
- # Provides an interface for any class to have +before_validation+ and
8
- # +after_validation+ callbacks.
7
+ # Provides an interface for any class to have ClassMethods#before_validation and
8
+ # ClassMethods#after_validation callbacks.
9
9
  #
10
- # First, include ActiveModel::Validations::Callbacks from the class you are
10
+ # First, include +ActiveModel::Validations::Callbacks+ from the class you are
11
11
  # creating:
12
12
  #
13
13
  # class MyModel
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model/validations/resolve_value"
3
4
  require "active_support/core_ext/range"
4
5
 
5
6
  module ActiveModel
6
7
  module Validations
7
8
  module Clusivity # :nodoc:
9
+ include ResolveValue
10
+
8
11
  ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
9
12
  "and must be supplied as the :in (or :within) option of the configuration hash"
10
13
 
@@ -16,13 +19,7 @@ module ActiveModel
16
19
 
17
20
  private
18
21
  def include?(record, value)
19
- members = if delimiter.respond_to?(:call)
20
- delimiter.call(record)
21
- elsif delimiter.respond_to?(:to_sym)
22
- record.send(delimiter)
23
- else
24
- delimiter
25
- end
22
+ members = resolve_value(record, delimiter)
26
23
 
27
24
  if value.is_a?(Array)
28
25
  value.all? { |v| members.public_send(inclusion_method(members), v) }
@@ -42,7 +39,7 @@ module ActiveModel
42
39
  # or DateTime ranges.
43
40
  def inclusion_method(enumerable)
44
41
  if enumerable.is_a? Range
45
- case enumerable.first
42
+ case enumerable.begin || enumerable.end
46
43
  when Numeric, Time, DateTime, Date
47
44
  :cover?
48
45
  else
@@ -7,17 +7,6 @@ module ActiveModel
7
7
  equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
8
8
  other_than: :!= }.freeze
9
9
 
10
- def option_value(record, option_value)
11
- case option_value
12
- when Proc
13
- option_value.call(record)
14
- when Symbol
15
- record.send(option_value)
16
- else
17
- option_value
18
- end
19
- end
20
-
21
10
  def error_options(value, option_value)
22
11
  options.except(*COMPARE_CHECKS.keys).merge!(
23
12
  count: option_value,
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/validations/comparability"
4
+ require "active_model/validations/resolve_value"
4
5
 
5
6
  module ActiveModel
6
7
  module Validations
7
8
  class ComparisonValidator < EachValidator # :nodoc:
8
9
  include Comparability
10
+ include ResolveValue
9
11
 
10
12
  def check_validity!
11
13
  unless (options.keys & COMPARE_CHECKS.keys).any?
@@ -16,7 +18,7 @@ module ActiveModel
16
18
 
17
19
  def validate_each(record, attr_name, value)
18
20
  options.slice(*COMPARE_CHECKS.keys).each do |option, raw_option_value|
19
- option_value = option_value(record, raw_option_value)
21
+ option_value = resolve_value(record, raw_option_value)
20
22
 
21
23
  if value.nil? || value.blank?
22
24
  return record.errors.add(attr_name, :blank, **error_options(value, option_value))
@@ -42,17 +44,23 @@ module ActiveModel
42
44
  # Configuration options:
43
45
  # * <tt>:message</tt> - A custom error message (default is: "failed comparison").
44
46
  # * <tt>:greater_than</tt> - Specifies the value must be greater than the
45
- # supplied value.
47
+ # supplied value. The default error message for this option is _"must be
48
+ # greater than %{count}"_.
46
49
  # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
47
- # greater than or equal to the supplied value.
50
+ # greater than or equal to the supplied value. The default error message
51
+ # for this option is _"must be greater than or equal to %{count}"_.
48
52
  # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
49
- # value.
53
+ # value. The default error message for this option is _"must be equal to
54
+ # %{count}"_.
50
55
  # * <tt>:less_than</tt> - Specifies the value must be less than the
51
- # supplied value.
56
+ # supplied value. The default error message for this option is _"must be
57
+ # less than %{count}"_.
52
58
  # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
53
- # than or equal to the supplied value.
59
+ # than or equal to the supplied value. The default error message for
60
+ # this option is _"must be less than or equal to %{count}"_.
54
61
  # * <tt>:other_than</tt> - Specifies the value must not be equal to the
55
- # supplied value.
62
+ # supplied value. The default error message for this option is _"must be
63
+ # other than %{count}"_.
56
64
  #
57
65
  # There is also a list of default options supported by every validator:
58
66
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model/validations/resolve_value"
4
+
3
5
  module ActiveModel
4
6
  module Validations
5
7
  class FormatValidator < EachValidator # :nodoc:
8
+ include ResolveValue
9
+
6
10
  def validate_each(record, attribute, value)
7
11
  if options[:with]
8
- regexp = option_call(record, :with)
12
+ regexp = resolve_value(record, options[:with])
9
13
  record_error(record, attribute, :with, value) unless regexp.match?(value.to_s)
10
14
  elsif options[:without]
11
- regexp = option_call(record, :without)
15
+ regexp = resolve_value(record, options[:without])
12
16
  record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
13
17
  end
14
18
  end
@@ -23,11 +27,6 @@ module ActiveModel
23
27
  end
24
28
 
25
29
  private
26
- def option_call(record, name)
27
- option = options[name]
28
- option.respond_to?(:call) ? option.call(record) : option
29
- end
30
-
31
30
  def record_error(record, attribute, name, value)
32
31
  record.errors.add(attribute, :invalid, **options.except(name).merge!(value: value))
33
32
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model/validations/resolve_value"
4
+
3
5
  module ActiveModel
4
6
  module Validations
5
7
  class LengthValidator < EachValidator # :nodoc:
8
+ include ResolveValue
9
+
6
10
  MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
7
11
  CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
8
12
 
@@ -11,7 +15,8 @@ module ActiveModel
11
15
  def initialize(options)
12
16
  if range = (options.delete(:in) || options.delete(:within))
13
17
  raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
14
- options[:minimum], options[:maximum] = range.min, range.max
18
+ options[:minimum] = range.min if range.begin
19
+ options[:maximum] = (range.exclude_end? ? range.end - 1 : range.end) if range.end
15
20
  end
16
21
 
17
22
  if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
@@ -31,7 +36,9 @@ module ActiveModel
31
36
  keys.each do |key|
32
37
  value = options[key]
33
38
 
34
- unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
39
+ unless (value.is_a?(Integer) && value >= 0) ||
40
+ value == Float::INFINITY || value == -Float::INFINITY ||
41
+ value.is_a?(Symbol) || value.is_a?(Proc)
35
42
  raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
36
43
  end
37
44
  end
@@ -45,12 +52,7 @@ module ActiveModel
45
52
  next unless check_value = options[key]
46
53
 
47
54
  if !value.nil? || skip_nil_check?(key)
48
- case check_value
49
- when Proc
50
- check_value = check_value.call(record)
51
- when Symbol
52
- check_value = record.send(check_value)
53
- end
55
+ check_value = resolve_value(record, check_value)
54
56
  next if value_length.public_send(validity_check, check_value)
55
57
  end
56
58
 
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/validations/comparability"
4
+ require "active_model/validations/resolve_value"
4
5
  require "bigdecimal/util"
5
6
 
6
7
  module ActiveModel
7
8
  module Validations
8
9
  class NumericalityValidator < EachValidator # :nodoc:
9
10
  include Comparability
11
+ include ResolveValue
10
12
 
11
13
  RANGE_CHECKS = { in: :in? }
12
14
  NUMBER_CHECKS = { odd: :odd?, even: :even? }
13
15
 
14
- RESERVED_OPTIONS = COMPARE_CHECKS.keys + NUMBER_CHECKS.keys + RANGE_CHECKS.keys + [:only_integer]
16
+ RESERVED_OPTIONS = COMPARE_CHECKS.keys + NUMBER_CHECKS.keys + RANGE_CHECKS.keys + [:only_integer, :only_numeric]
15
17
 
16
18
  INTEGER_REGEX = /\A[+-]?\d+\z/
17
19
 
@@ -64,7 +66,7 @@ module ActiveModel
64
66
 
65
67
  private
66
68
  def option_as_number(record, option_value, precision, scale)
67
- parse_as_number(option_value(record, option_value), precision, scale)
69
+ parse_as_number(resolve_value(record, option_value), precision, scale)
68
70
  end
69
71
 
70
72
  def parse_as_number(raw_value, precision, scale)
@@ -90,6 +92,10 @@ module ActiveModel
90
92
  end
91
93
 
92
94
  def is_number?(raw_value, precision, scale)
95
+ if options[:only_numeric] && !raw_value.is_a?(Numeric)
96
+ return false
97
+ end
98
+
93
99
  !parse_as_number(raw_value, precision, scale).nil?
94
100
  rescue ArgumentError, TypeError
95
101
  false
@@ -110,14 +116,7 @@ module ActiveModel
110
116
  end
111
117
 
112
118
  def allow_only_integer?(record)
113
- case options[:only_integer]
114
- when Symbol
115
- record.send(options[:only_integer])
116
- when Proc
117
- options[:only_integer].call(record)
118
- else
119
- options[:only_integer]
120
- end
119
+ resolve_value(record, options[:only_integer])
121
120
  end
122
121
 
123
122
  def prepare_value_for_validation(value, record, attr_name)
@@ -149,10 +148,11 @@ module ActiveModel
149
148
 
150
149
  module HelperMethods
151
150
  # Validates whether the value of the specified attribute is numeric by
152
- # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
153
- # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
154
- # (if <tt>only_integer</tt> is set to +true+). Precision of Kernel.Float values
155
- # are guaranteed up to 15 digits.
151
+ # trying to convert it to a float with +Kernel.Float+ (if
152
+ # <tt>only_integer</tt> is +false+) or applying it to the regular
153
+ # expression <tt>/\A[\+\-]?\d+\z/</tt> (if <tt>only_integer</tt> is set to
154
+ # +true+). Precision of +Kernel.Float+ values are guaranteed up to 15
155
+ # digits.
156
156
  #
157
157
  # class Person < ActiveRecord::Base
158
158
  # validates_numericality_of :value, on: :create
@@ -162,24 +162,36 @@ module ActiveModel
162
162
  # * <tt>:message</tt> - A custom error message (default is: "is not a number").
163
163
  # * <tt>:only_integer</tt> - Specifies whether the value has to be an
164
164
  # integer (default is +false+).
165
+ # * <tt>:only_numeric</tt> - Specifies whether the value has to be an
166
+ # instance of Numeric (default is +false+). The default behavior is to
167
+ # attempt parsing the value if it is a String.
165
168
  # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
166
169
  # +false+). Notice that for Integer and Float columns empty strings are
167
170
  # converted to +nil+.
168
171
  # * <tt>:greater_than</tt> - Specifies the value must be greater than the
169
- # supplied value.
172
+ # supplied value. The default error message for this option is _"must be
173
+ # greater than %{count}"_.
170
174
  # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
171
- # greater than or equal the supplied value.
175
+ # greater than or equal the supplied value. The default error message
176
+ # for this option is _"must be greater than or equal to %{count}"_.
172
177
  # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
173
- # value.
178
+ # value. The default error message for this option is _"must be equal to
179
+ # %{count}"_.
174
180
  # * <tt>:less_than</tt> - Specifies the value must be less than the
175
- # supplied value.
181
+ # supplied value. The default error message for this option is _"must be
182
+ # less than %{count}"_.
176
183
  # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
177
- # than or equal the supplied value.
184
+ # than or equal the supplied value. The default error message for this
185
+ # option is _"must be less than or equal to %{count}"_.
178
186
  # * <tt>:other_than</tt> - Specifies the value must be other than the
179
- # supplied value.
180
- # * <tt>:odd</tt> - Specifies the value must be an odd number.
181
- # * <tt>:even</tt> - Specifies the value must be an even number.
182
- # * <tt>:in</tt> - Check that the value is within a range.
187
+ # supplied value. The default error message for this option is _"must be
188
+ # other than %{count}"_.
189
+ # * <tt>:odd</tt> - Specifies the value must be an odd number. The default
190
+ # error message for this option is _"must be odd"_.
191
+ # * <tt>:even</tt> - Specifies the value must be an even number. The
192
+ # default error message for this option is _"must be even"_.
193
+ # * <tt>:in</tt> - Check that the value is within a range. The default
194
+ # error message for this option is _"must be in %{count}"_.
183
195
  #
184
196
  # There is also a list of default options supported by every validator:
185
197
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
@@ -10,7 +10,7 @@ module ActiveModel
10
10
 
11
11
  module HelperMethods
12
12
  # Validates that the specified attributes are not blank (as defined by
13
- # Object#blank?). Happens by default on save.
13
+ # Object#blank?).
14
14
  #
15
15
  # class Person < ActiveRecord::Base
16
16
  # validates_presence_of :first_name