omg-activemodel 8.0.0.alpha1

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class ConfirmationValidator < EachValidator # :nodoc:
6
+ def initialize(options)
7
+ super({ case_sensitive: true }.merge!(options))
8
+ setup!(options[:class])
9
+ end
10
+
11
+ def validate_each(record, attribute, value)
12
+ unless (confirmed = record.public_send("#{attribute}_confirmation")).nil?
13
+ unless confirmation_value_equal?(record, attribute, value, confirmed)
14
+ human_attribute_name = record.class.human_attribute_name(attribute)
15
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, **options.except(:case_sensitive).merge!(attribute: human_attribute_name))
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+ def setup!(klass)
22
+ klass.attr_reader(*attributes.filter_map do |attribute|
23
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
24
+ end)
25
+
26
+ klass.attr_writer(*attributes.filter_map do |attribute|
27
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
28
+ end)
29
+ end
30
+
31
+ def confirmation_value_equal?(record, attribute, value, confirmed)
32
+ if !options[:case_sensitive] && value.is_a?(String)
33
+ value.casecmp(confirmed) == 0
34
+ else
35
+ value == confirmed
36
+ end
37
+ end
38
+ end
39
+
40
+ module HelperMethods
41
+ # Encapsulates the pattern of wanting to validate a password or email
42
+ # address field with a confirmation.
43
+ #
44
+ # Model:
45
+ # class Person < ActiveRecord::Base
46
+ # validates_confirmation_of :user_name, :password
47
+ # validates_confirmation_of :email_address,
48
+ # message: 'should match confirmation'
49
+ # end
50
+ #
51
+ # View:
52
+ # <%= password_field "person", "password" %>
53
+ # <%= password_field "person", "password_confirmation" %>
54
+ #
55
+ # The added +password_confirmation+ attribute is virtual; it exists only
56
+ # as an in-memory attribute for validating the password. To achieve this,
57
+ # the validation adds accessors to the model for the confirmation
58
+ # attribute.
59
+ #
60
+ # NOTE: This check is performed only if +password_confirmation+ is not
61
+ # +nil+. To require confirmation, make sure to add a presence check for
62
+ # the confirmation attribute:
63
+ #
64
+ # validates_presence_of :password_confirmation, if: :password_changed?
65
+ #
66
+ # Configuration options:
67
+ # * <tt>:message</tt> - A custom error message (default is: "doesn't match
68
+ # <tt>%{translated_attribute_name}</tt>").
69
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
70
+ # non-text columns (+true+ by default).
71
+ #
72
+ # There is also a list of default options supported by every validator:
73
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
74
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
75
+ def validates_confirmation_of(*attr_names)
76
+ validates_with ConfirmationValidator, _merge_attributes(attr_names)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/clusivity"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class ExclusionValidator < EachValidator # :nodoc:
8
+ include Clusivity
9
+
10
+ def validate_each(record, attribute, value)
11
+ if include?(record, value)
12
+ record.errors.add(attribute, :exclusion, **options.except(:in, :within).merge!(value: value))
13
+ end
14
+ end
15
+ end
16
+
17
+ module HelperMethods
18
+ # Validates that the value of the specified attribute is not in a
19
+ # particular enumerable object.
20
+ #
21
+ # class Person < ActiveRecord::Base
22
+ # validates_exclusion_of :username, in: %w( admin superuser ), message: "You don't belong here"
23
+ # validates_exclusion_of :age, in: 30..60, message: 'This site is only for under 30 and over 60'
24
+ # validates_exclusion_of :format, in: %w( mov avi ), message: "extension %{value} is not allowed"
25
+ # validates_exclusion_of :password, in: ->(person) { [person.username, person.first_name] },
26
+ # message: 'should not be the same as your username or first name'
27
+ # validates_exclusion_of :karma, in: :reserved_karmas
28
+ # end
29
+ #
30
+ # Configuration options:
31
+ # * <tt>:in</tt> - An enumerable object of items that the value shouldn't
32
+ # be part of. This can be supplied as a proc, lambda, or symbol which returns an
33
+ # enumerable. If the enumerable is a numerical, time, or datetime range the test
34
+ # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When
35
+ # using a proc or lambda the instance under validation is passed as an argument.
36
+ # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
37
+ # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
38
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is
39
+ # reserved").
40
+ #
41
+ # There is also a list of default options supported by every validator:
42
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
43
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
44
+ def validates_exclusion_of(*attr_names)
45
+ validates_with ExclusionValidator, _merge_attributes(attr_names)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/resolve_value"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class FormatValidator < EachValidator # :nodoc:
8
+ include ResolveValue
9
+
10
+ def validate_each(record, attribute, value)
11
+ if options[:with]
12
+ regexp = resolve_value(record, options[:with])
13
+ record_error(record, attribute, :with, value) unless regexp.match?(value.to_s)
14
+ elsif options[:without]
15
+ regexp = resolve_value(record, options[:without])
16
+ record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
17
+ end
18
+ end
19
+
20
+ def check_validity!
21
+ unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
22
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)"
23
+ end
24
+
25
+ check_options_validity :with
26
+ check_options_validity :without
27
+ end
28
+
29
+ private
30
+ def record_error(record, attribute, name, value)
31
+ record.errors.add(attribute, :invalid, **options.except(name).merge!(value: value))
32
+ end
33
+
34
+ def check_options_validity(name)
35
+ if option = options[name]
36
+ if option.is_a?(Regexp)
37
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
38
+ raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
39
+ "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
40
+ ":multiline => true option?"
41
+ end
42
+ elsif !option.respond_to?(:call)
43
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
44
+ end
45
+ end
46
+ end
47
+
48
+ def regexp_using_multiline_anchors?(regexp)
49
+ source = regexp.source
50
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
51
+ end
52
+ end
53
+
54
+ module HelperMethods
55
+ # Validates whether the value of the specified attribute is of the correct
56
+ # form, going by the regular expression provided. You can require that the
57
+ # attribute matches the regular expression:
58
+ #
59
+ # class Person < ActiveRecord::Base
60
+ # validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create
61
+ # end
62
+ #
63
+ # Alternatively, you can require that the specified attribute does _not_
64
+ # match the regular expression:
65
+ #
66
+ # class Person < ActiveRecord::Base
67
+ # validates_format_of :email, without: /NOSPAM/
68
+ # end
69
+ #
70
+ # You can also provide a proc or lambda which will determine the regular
71
+ # expression that will be used to validate the attribute.
72
+ #
73
+ # class Person < ActiveRecord::Base
74
+ # # Admin can have number as a first letter in their screen name
75
+ # validates_format_of :screen_name,
76
+ # with: ->(person) { person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i }
77
+ # end
78
+ #
79
+ # Note: use <tt>\A</tt> and <tt>\z</tt> to match the start and end of the
80
+ # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
81
+ #
82
+ # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass
83
+ # the <tt>multiline: true</tt> option in case you use any of these two
84
+ # anchors in the provided regular expression. In most cases, you should be
85
+ # using <tt>\A</tt> and <tt>\z</tt>.
86
+ #
87
+ # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option.
88
+ # In addition, both must be a regular expression or a proc or lambda, or
89
+ # else an exception will be raised.
90
+ #
91
+ # Configuration options:
92
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
93
+ # * <tt>:with</tt> - Regular expression that if the attribute matches will
94
+ # result in a successful validation. This can be provided as a proc or
95
+ # lambda returning regular expression which will be called at runtime.
96
+ # * <tt>:without</tt> - Regular expression that if the attribute does not
97
+ # match will result in a successful validation. This can be provided as
98
+ # a proc or lambda returning regular expression which will be called at
99
+ # runtime.
100
+ # * <tt>:multiline</tt> - Set to true if your regular expression contains
101
+ # anchors that match the beginning or end of lines as opposed to the
102
+ # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
103
+ #
104
+ # There is also a list of default options supported by every validator:
105
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
106
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
107
+ def validates_format_of(*attr_names)
108
+ validates_with FormatValidator, _merge_attributes(attr_names)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ module HelperMethods # :nodoc:
6
+ private
7
+ def _merge_attributes(attr_names)
8
+ options = attr_names.extract_options!.symbolize_keys
9
+ attr_names.flatten!
10
+ options[:attributes] = attr_names
11
+ options
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/clusivity"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class InclusionValidator < EachValidator # :nodoc:
8
+ include Clusivity
9
+
10
+ def validate_each(record, attribute, value)
11
+ unless include?(record, value)
12
+ record.errors.add(attribute, :inclusion, **options.except(:in, :within).merge!(value: value))
13
+ end
14
+ end
15
+ end
16
+
17
+ module HelperMethods
18
+ # Validates whether the value of the specified attribute is available in a
19
+ # particular enumerable object.
20
+ #
21
+ # class Person < ActiveRecord::Base
22
+ # validates_inclusion_of :role, in: %w( admin contributor )
23
+ # validates_inclusion_of :age, in: 0..99
24
+ # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
25
+ # validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
26
+ # validates_inclusion_of :karma, in: :available_karmas
27
+ # end
28
+ #
29
+ # Configuration options:
30
+ # * <tt>:in</tt> - An enumerable object of available items. This can be
31
+ # supplied as a proc, lambda, or symbol which returns an enumerable. If the
32
+ # enumerable is a numerical, time, or datetime range the test is performed
33
+ # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using
34
+ # a proc or lambda the instance under validation is passed as an argument.
35
+ # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
36
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is
37
+ # not included in the list").
38
+ #
39
+ # There is also a list of default options supported by every validator:
40
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
41
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
42
+ def validates_inclusion_of(*attr_names)
43
+ validates_with InclusionValidator, _merge_attributes(attr_names)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/resolve_value"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class LengthValidator < EachValidator # :nodoc:
8
+ include ResolveValue
9
+
10
+ MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
11
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
12
+
13
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
14
+
15
+ def initialize(options)
16
+ if range = (options.delete(:in) || options.delete(:within))
17
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
18
+ options[:minimum] = range.min if range.begin
19
+ options[:maximum] = (range.exclude_end? ? range.end - 1 : range.end) if range.end
20
+ end
21
+
22
+ if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
23
+ options[:minimum] = 1
24
+ end
25
+
26
+ super
27
+ end
28
+
29
+ def check_validity!
30
+ keys = CHECKS.keys & options.keys
31
+
32
+ if keys.empty?
33
+ raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
34
+ end
35
+
36
+ keys.each do |key|
37
+ value = options[key]
38
+
39
+ unless (value.is_a?(Integer) && value >= 0) ||
40
+ value == Float::INFINITY || value == -Float::INFINITY ||
41
+ value.is_a?(Symbol) || value.is_a?(Proc)
42
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
43
+ end
44
+ end
45
+ end
46
+
47
+ def validate_each(record, attribute, value)
48
+ value_length = value.respond_to?(:length) ? value.length : value.to_s.length
49
+ errors_options = options.except(*RESERVED_OPTIONS)
50
+
51
+ CHECKS.each do |key, validity_check|
52
+ next unless check_value = options[key]
53
+
54
+ if !value.nil? || skip_nil_check?(key)
55
+ check_value = resolve_value(record, check_value)
56
+ next if value_length.public_send(validity_check, check_value)
57
+ end
58
+
59
+ errors_options[:count] = check_value
60
+
61
+ default_message = options[MESSAGES[key]]
62
+ errors_options[:message] ||= default_message if default_message
63
+
64
+ record.errors.add(attribute, MESSAGES[key], **errors_options)
65
+ end
66
+ end
67
+
68
+ private
69
+ def skip_nil_check?(key)
70
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
71
+ end
72
+ end
73
+
74
+ module HelperMethods
75
+ # Validates that the specified attributes match the length restrictions
76
+ # supplied. Only one constraint option can be used at a time apart from
77
+ # +:minimum+ and +:maximum+ that can be combined together:
78
+ #
79
+ # class Person < ActiveRecord::Base
80
+ # validates_length_of :first_name, maximum: 30
81
+ # validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind"
82
+ # validates_length_of :fax, in: 7..32, allow_nil: true
83
+ # validates_length_of :phone, in: 7..32, allow_blank: true
84
+ # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
85
+ # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
86
+ # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
87
+ # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.'
88
+ #
89
+ # private
90
+ # def words_in_essay
91
+ # essay.scan(/\w+/)
92
+ # end
93
+ # end
94
+ #
95
+ # Constraint options:
96
+ #
97
+ # * <tt>:minimum</tt> - The minimum size of the attribute.
98
+ # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
99
+ # default if not used with +:minimum+.
100
+ # * <tt>:is</tt> - The exact size of the attribute.
101
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of
102
+ # the attribute.
103
+ # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
104
+ #
105
+ # Other options:
106
+ #
107
+ # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
108
+ # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
109
+ # * <tt>:too_long</tt> - The error message if the attribute goes over the
110
+ # maximum (default is: "is too long (maximum is %{count} characters)").
111
+ # * <tt>:too_short</tt> - The error message if the attribute goes under the
112
+ # minimum (default is: "is too short (minimum is %{count} characters)").
113
+ # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
114
+ # method and the attribute is the wrong size (default is: "is the wrong
115
+ # length (should be %{count} characters)").
116
+ # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
117
+ # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
118
+ # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
119
+ #
120
+ # There is also a list of default options supported by every validator:
121
+ # +:if+, +:unless+, +:on+, and +:strict+.
122
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
123
+ def validates_length_of(*attr_names)
124
+ validates_with LengthValidator, _merge_attributes(attr_names)
125
+ end
126
+
127
+ alias_method :validates_size_of, :validates_length_of
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/comparability"
4
+ require "active_model/validations/resolve_value"
5
+ require "bigdecimal/util"
6
+
7
+ module ActiveModel
8
+ module Validations
9
+ class NumericalityValidator < EachValidator # :nodoc:
10
+ include Comparability
11
+ include ResolveValue
12
+
13
+ RANGE_CHECKS = { in: :in? }
14
+ NUMBER_CHECKS = { odd: :odd?, even: :even? }
15
+
16
+ RESERVED_OPTIONS = COMPARE_CHECKS.keys + NUMBER_CHECKS.keys + RANGE_CHECKS.keys + [:only_integer, :only_numeric]
17
+
18
+ INTEGER_REGEX = /\A[+-]?\d+\z/
19
+
20
+ HEXADECIMAL_REGEX = /\A[+-]?0[xX]/
21
+
22
+ def check_validity!
23
+ options.slice(*COMPARE_CHECKS.keys).each do |option, value|
24
+ unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
25
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
26
+ end
27
+ end
28
+
29
+ options.slice(*RANGE_CHECKS.keys).each do |option, value|
30
+ unless value.is_a?(Range)
31
+ raise ArgumentError, ":#{option} must be a range"
32
+ end
33
+ end
34
+ end
35
+
36
+ def validate_each(record, attr_name, value, precision: Float::DIG, scale: nil)
37
+ unless is_number?(value, precision, scale)
38
+ record.errors.add(attr_name, :not_a_number, **filtered_options(value))
39
+ return
40
+ end
41
+
42
+ if allow_only_integer?(record) && !is_integer?(value)
43
+ record.errors.add(attr_name, :not_an_integer, **filtered_options(value))
44
+ return
45
+ end
46
+
47
+ value = parse_as_number(value, precision, scale)
48
+
49
+ options.slice(*RESERVED_OPTIONS).each do |option, option_value|
50
+ if NUMBER_CHECKS.include?(option)
51
+ unless value.to_i.public_send(NUMBER_CHECKS[option])
52
+ record.errors.add(attr_name, option, **filtered_options(value))
53
+ end
54
+ elsif RANGE_CHECKS.include?(option)
55
+ unless value.public_send(RANGE_CHECKS[option], option_value)
56
+ record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
57
+ end
58
+ elsif COMPARE_CHECKS.include?(option)
59
+ option_value = option_as_number(record, option_value, precision, scale)
60
+ unless value.public_send(COMPARE_CHECKS[option], option_value)
61
+ record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+ def option_as_number(record, option_value, precision, scale)
69
+ parse_as_number(resolve_value(record, option_value), precision, scale)
70
+ end
71
+
72
+ def parse_as_number(raw_value, precision, scale)
73
+ if raw_value.is_a?(Float)
74
+ parse_float(raw_value, precision, scale)
75
+ elsif raw_value.is_a?(BigDecimal)
76
+ round(raw_value, scale)
77
+ elsif raw_value.is_a?(Numeric)
78
+ raw_value
79
+ elsif is_integer?(raw_value)
80
+ raw_value.to_i
81
+ elsif !is_hexadecimal_literal?(raw_value)
82
+ parse_float(Kernel.Float(raw_value), precision, scale)
83
+ end
84
+ end
85
+
86
+ def parse_float(raw_value, precision, scale)
87
+ round(raw_value, scale).to_d(precision)
88
+ end
89
+
90
+ def round(raw_value, scale)
91
+ scale ? raw_value.round(scale) : raw_value
92
+ end
93
+
94
+ def is_number?(raw_value, precision, scale)
95
+ if options[:only_numeric] && !raw_value.is_a?(Numeric)
96
+ return false
97
+ end
98
+
99
+ !parse_as_number(raw_value, precision, scale).nil?
100
+ rescue ArgumentError, TypeError
101
+ false
102
+ end
103
+
104
+ def is_integer?(raw_value)
105
+ INTEGER_REGEX.match?(raw_value.to_s)
106
+ end
107
+
108
+ def is_hexadecimal_literal?(raw_value)
109
+ HEXADECIMAL_REGEX.match?(raw_value.to_s)
110
+ end
111
+
112
+ def filtered_options(value)
113
+ filtered = options.except(*RESERVED_OPTIONS)
114
+ filtered[:value] = value
115
+ filtered
116
+ end
117
+
118
+ def allow_only_integer?(record)
119
+ resolve_value(record, options[:only_integer])
120
+ end
121
+
122
+ def prepare_value_for_validation(value, record, attr_name)
123
+ return value if record_attribute_changed_in_place?(record, attr_name)
124
+
125
+ came_from_user = :"#{attr_name}_came_from_user?"
126
+
127
+ if record.respond_to?(came_from_user)
128
+ if record.public_send(came_from_user)
129
+ raw_value = record.public_send(:"#{attr_name}_before_type_cast")
130
+ elsif record.respond_to?(:read_attribute)
131
+ raw_value = record.read_attribute(attr_name)
132
+ end
133
+ else
134
+ before_type_cast = :"#{attr_name}_before_type_cast"
135
+ if record.respond_to?(before_type_cast)
136
+ raw_value = record.public_send(before_type_cast)
137
+ end
138
+ end
139
+
140
+ raw_value || value
141
+ end
142
+
143
+ def record_attribute_changed_in_place?(record, attr_name)
144
+ record.respond_to?(:attribute_changed_in_place?) &&
145
+ record.attribute_changed_in_place?(attr_name.to_s)
146
+ end
147
+ end
148
+
149
+ module HelperMethods
150
+ # Validates whether the value of the specified attribute is numeric by
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
+ #
157
+ # class Person < ActiveRecord::Base
158
+ # validates_numericality_of :value, on: :create
159
+ # end
160
+ #
161
+ # Configuration options:
162
+ # * <tt>:message</tt> - A custom error message (default is: "is not a number").
163
+ # * <tt>:only_integer</tt> - Specifies whether the value has to be an
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.
168
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
169
+ # +false+). Notice that for Integer and Float columns empty strings are
170
+ # converted to +nil+.
171
+ # * <tt>:greater_than</tt> - Specifies the value must be greater than the
172
+ # supplied value. The default error message for this option is _"must be
173
+ # greater than %{count}"_.
174
+ # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
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}"_.
177
+ # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
178
+ # value. The default error message for this option is _"must be equal to
179
+ # %{count}"_.
180
+ # * <tt>:less_than</tt> - Specifies the value must be less than the
181
+ # supplied value. The default error message for this option is _"must be
182
+ # less than %{count}"_.
183
+ # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
184
+ # than or equal the supplied value. The default error message for this
185
+ # option is _"must be less than or equal to %{count}"_.
186
+ # * <tt>:other_than</tt> - Specifies the value must be other than the
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}"_.
195
+ #
196
+ # There is also a list of default options supported by every validator:
197
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
198
+ # See ActiveModel::Validations::ClassMethods#validates for more information.
199
+ #
200
+ # The following checks can also be supplied with a proc or a symbol which
201
+ # corresponds to a method:
202
+ #
203
+ # * <tt>:greater_than</tt>
204
+ # * <tt>:greater_than_or_equal_to</tt>
205
+ # * <tt>:equal_to</tt>
206
+ # * <tt>:less_than</tt>
207
+ # * <tt>:less_than_or_equal_to</tt>
208
+ # * <tt>:only_integer</tt>
209
+ # * <tt>:other_than</tt>
210
+ #
211
+ # For example:
212
+ #
213
+ # class Person < ActiveRecord::Base
214
+ # validates_numericality_of :width, less_than: ->(person) { person.height }
215
+ # validates_numericality_of :width, greater_than: :minimum_weight
216
+ # end
217
+ def validates_numericality_of(*attr_names)
218
+ validates_with NumericalityValidator, _merge_attributes(attr_names)
219
+ end
220
+ end
221
+ end
222
+ end