activemodel 7.0.8.1 → 7.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -196
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  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 +102 -63
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +9 -0
  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 +4 -3
  19. data/lib/active_model/errors.rb +17 -12
  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 +4 -3
  24. data/lib/active_model/model.rb +26 -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 +7 -1
  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/numeric.rb +4 -0
  40. data/lib/active_model/type/helpers/time_value.rb +28 -12
  41. data/lib/active_model/type/immutable_string.rb +37 -1
  42. data/lib/active_model/type/integer.rb +44 -1
  43. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  44. data/lib/active_model/type/string.rb +9 -1
  45. data/lib/active_model/type/time.rb +48 -7
  46. data/lib/active_model/type/value.rb +17 -1
  47. data/lib/active_model/type.rb +1 -0
  48. data/lib/active_model/validations/absence.rb +1 -1
  49. data/lib/active_model/validations/acceptance.rb +1 -1
  50. data/lib/active_model/validations/callbacks.rb +4 -4
  51. data/lib/active_model/validations/clusivity.rb +5 -8
  52. data/lib/active_model/validations/comparability.rb +0 -11
  53. data/lib/active_model/validations/comparison.rb +15 -7
  54. data/lib/active_model/validations/confirmation.rb +1 -1
  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 +2 -2
  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 +45 -10
  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 +15 -10
@@ -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
@@ -129,6 +141,10 @@ module ActiveModel
129
141
  false
130
142
  end
131
143
 
144
+ def as_json(*)
145
+ raise NoMethodError
146
+ end
147
+
132
148
  private
133
149
  # Convenience method for types which do not need separate type casting
134
150
  # behavior for user and database inputs. Called by Value#cast for
@@ -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+ .
@@ -64,7 +64,7 @@ module ActiveModel
64
64
  # validates_presence_of :password_confirmation, if: :password_changed?
65
65
  #
66
66
  # Configuration options:
67
- # * <tt>:message</tt> - A custom error message (default is: "doesn't match
67
+ # * <tt>:message</tt> - A custom error message (default is: "doesnt match
68
68
  # <tt>%{translated_attribute_name}</tt>").
69
69
  # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
70
70
  # non-text columns (+true+ by default).
@@ -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
@@ -26,7 +26,7 @@ module ActiveModel
26
26
  # <tt>false.blank? # => true</tt>.
27
27
  #
28
28
  # Configuration options:
29
- # * <tt>:message</tt> - A custom error message (default is: "can't be blank").
29
+ # * <tt>:message</tt> - A custom error message (default is: "cant be blank").
30
30
  #
31
31
  # There is also a list of default options supported by every validator:
32
32
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ module ResolveValue # :nodoc:
6
+ def resolve_value(record, value)
7
+ case value
8
+ when Proc
9
+ if value.arity == 0
10
+ value.call
11
+ else
12
+ value.call(record)
13
+ end
14
+ when Symbol
15
+ record.send(value)
16
+ else
17
+ if value.respond_to?(:call)
18
+ value.call(record)
19
+ else
20
+ value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end