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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +67 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +266 -0
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +99 -0
- data/lib/active_model/attribute/user_provided_default.rb +55 -0
- data/lib/active_model/attribute.rb +277 -0
- data/lib/active_model/attribute_assignment.rb +78 -0
- data/lib/active_model/attribute_methods.rb +592 -0
- data/lib/active_model/attribute_mutation_tracker.rb +189 -0
- data/lib/active_model/attribute_registration.rb +117 -0
- data/lib/active_model/attribute_set/builder.rb +182 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +118 -0
- data/lib/active_model/attributes.rb +165 -0
- data/lib/active_model/callbacks.rb +155 -0
- data/lib/active_model/conversion.rb +121 -0
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +416 -0
- data/lib/active_model/error.rb +208 -0
- data/lib/active_model/errors.rb +547 -0
- data/lib/active_model/forbidden_attributes_protection.rb +33 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +38 -0
- data/lib/active_model/model.rb +78 -0
- data/lib/active_model/naming.rb +359 -0
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +24 -0
- data/lib/active_model/secure_password.rb +231 -0
- data/lib/active_model/serialization.rb +198 -0
- data/lib/active_model/serializers/json.rb +154 -0
- data/lib/active_model/translation.rb +78 -0
- data/lib/active_model/type/big_integer.rb +36 -0
- data/lib/active_model/type/binary.rb +62 -0
- data/lib/active_model/type/boolean.rb +48 -0
- data/lib/active_model/type/date.rb +78 -0
- data/lib/active_model/type/date_time.rb +88 -0
- data/lib/active_model/type/decimal.rb +107 -0
- data/lib/active_model/type/float.rb +64 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
- data/lib/active_model/type/helpers/mutable.rb +24 -0
- data/lib/active_model/type/helpers/numeric.rb +61 -0
- data/lib/active_model/type/helpers/time_value.rb +127 -0
- data/lib/active_model/type/helpers/timezone.rb +23 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +71 -0
- data/lib/active_model/type/integer.rb +113 -0
- data/lib/active_model/type/registry.rb +37 -0
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +43 -0
- data/lib/active_model/type/time.rb +87 -0
- data/lib/active_model/type/value.rb +157 -0
- data/lib/active_model/type.rb +55 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +113 -0
- data/lib/active_model/validations/callbacks.rb +119 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/comparability.rb +18 -0
- data/lib/active_model/validations/comparison.rb +90 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +112 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +130 -0
- data/lib/active_model/validations/numericality.rb +222 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +175 -0
- data/lib/active_model/validations/with.rb +154 -0
- data/lib/active_model/validations.rb +489 -0
- data/lib/active_model/validator.rb +190 -0
- data/lib/active_model/version.rb +10 -0
- data/lib/active_model.rb +84 -0
- 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
|