activemodel 6.0.0

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +172 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +247 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +517 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +178 -0
  11. data/lib/active_model/attribute_set.rb +106 -0
  12. data/lib/active_model/attribute_set/builder.rb +124 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  14. data/lib/active_model/attributes.rb +138 -0
  15. data/lib/active_model/callbacks.rb +156 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +280 -0
  18. data/lib/active_model/errors.rb +601 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +334 -0
  25. data/lib/active_model/railtie.rb +20 -0
  26. data/lib/active_model/secure_password.rb +128 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +147 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +47 -0
  34. data/lib/active_model/type/date.rb +53 -0
  35. data/lib/active_model/type/date_time.rb +47 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +34 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +45 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +44 -0
  42. data/lib/active_model/type/helpers/time_value.rb +81 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +58 -0
  46. data/lib/active_model/type/registry.rb +62 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +47 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +437 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +102 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -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 <tt>ActiveModel::Validations#validates</tt> 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,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class LengthValidator < EachValidator # :nodoc:
6
+ MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
7
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
8
+
9
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
10
+
11
+ def initialize(options)
12
+ if range = (options.delete(:in) || options.delete(:within))
13
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
14
+ options[:minimum], options[:maximum] = range.min, range.max
15
+ end
16
+
17
+ if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
18
+ options[:minimum] = 1
19
+ end
20
+
21
+ super
22
+ end
23
+
24
+ def check_validity!
25
+ keys = CHECKS.keys & options.keys
26
+
27
+ if keys.empty?
28
+ raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
29
+ end
30
+
31
+ keys.each do |key|
32
+ value = options[key]
33
+
34
+ unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
35
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
36
+ end
37
+ end
38
+ end
39
+
40
+ def validate_each(record, attribute, value)
41
+ value_length = value.respond_to?(:length) ? value.length : value.to_s.length
42
+ errors_options = options.except(*RESERVED_OPTIONS)
43
+
44
+ CHECKS.each do |key, validity_check|
45
+ next unless check_value = options[key]
46
+
47
+ 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
54
+ next if value_length.send(validity_check, check_value)
55
+ end
56
+
57
+ errors_options[:count] = check_value
58
+
59
+ default_message = options[MESSAGES[key]]
60
+ errors_options[:message] ||= default_message if default_message
61
+
62
+ record.errors.add(attribute, MESSAGES[key], errors_options)
63
+ end
64
+ end
65
+
66
+ private
67
+ def skip_nil_check?(key)
68
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
69
+ end
70
+ end
71
+
72
+ module HelperMethods
73
+ # Validates that the specified attributes match the length restrictions
74
+ # supplied. Only one constraint option can be used at a time apart from
75
+ # +:minimum+ and +:maximum+ that can be combined together:
76
+ #
77
+ # class Person < ActiveRecord::Base
78
+ # validates_length_of :first_name, maximum: 30
79
+ # validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind"
80
+ # validates_length_of :fax, in: 7..32, allow_nil: true
81
+ # validates_length_of :phone, in: 7..32, allow_blank: true
82
+ # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
83
+ # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
84
+ # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
85
+ # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.'
86
+ #
87
+ # private
88
+ #
89
+ # def words_in_essay
90
+ # essay.scan(/\w+/)
91
+ # end
92
+ # end
93
+ #
94
+ # Constraint options:
95
+ #
96
+ # * <tt>:minimum</tt> - The minimum size of the attribute.
97
+ # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
98
+ # default if not used with +:minimum+.
99
+ # * <tt>:is</tt> - The exact size of the attribute.
100
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of
101
+ # the attribute.
102
+ # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
103
+ #
104
+ # Other options:
105
+ #
106
+ # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
107
+ # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
108
+ # * <tt>:too_long</tt> - The error message if the attribute goes over the
109
+ # maximum (default is: "is too long (maximum is %{count} characters)").
110
+ # * <tt>:too_short</tt> - The error message if the attribute goes under the
111
+ # minimum (default is: "is too short (minimum is %{count} characters)").
112
+ # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
113
+ # method and the attribute is the wrong size (default is: "is the wrong
114
+ # length (should be %{count} characters)").
115
+ # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
116
+ # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
117
+ # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
118
+ #
119
+ # There is also a list of default options supported by every validator:
120
+ # +:if+, +:unless+, +:on+ and +:strict+.
121
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
122
+ def validates_length_of(*attr_names)
123
+ validates_with LengthValidator, _merge_attributes(attr_names)
124
+ end
125
+
126
+ alias_method :validates_size_of, :validates_length_of
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal/util"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class NumericalityValidator < EachValidator # :nodoc:
8
+ CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
9
+ equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
10
+ odd: :odd?, even: :even?, other_than: :!= }.freeze
11
+
12
+ RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
13
+
14
+ INTEGER_REGEX = /\A[+-]?\d+\z/
15
+
16
+ def check_validity!
17
+ keys = CHECKS.keys - [:odd, :even]
18
+ options.slice(*keys).each do |option, value|
19
+ unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
20
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
21
+ end
22
+ end
23
+ end
24
+
25
+ def validate_each(record, attr_name, value)
26
+ came_from_user = :"#{attr_name}_came_from_user?"
27
+
28
+ if record.respond_to?(came_from_user)
29
+ if record.public_send(came_from_user)
30
+ raw_value = record.read_attribute_before_type_cast(attr_name)
31
+ elsif record.respond_to?(:read_attribute)
32
+ raw_value = record.read_attribute(attr_name)
33
+ end
34
+ else
35
+ before_type_cast = :"#{attr_name}_before_type_cast"
36
+ if record.respond_to?(before_type_cast)
37
+ raw_value = record.public_send(before_type_cast)
38
+ end
39
+ end
40
+ raw_value ||= value
41
+
42
+ if record_attribute_changed_in_place?(record, attr_name)
43
+ raw_value = value
44
+ end
45
+
46
+ unless is_number?(raw_value)
47
+ record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
48
+ return
49
+ end
50
+
51
+ if allow_only_integer?(record) && !is_integer?(raw_value)
52
+ record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
53
+ return
54
+ end
55
+
56
+ value = parse_as_number(raw_value)
57
+
58
+ options.slice(*CHECKS.keys).each do |option, option_value|
59
+ case option
60
+ when :odd, :even
61
+ unless value.to_i.send(CHECKS[option])
62
+ record.errors.add(attr_name, option, filtered_options(value))
63
+ end
64
+ else
65
+ case option_value
66
+ when Proc
67
+ option_value = option_value.call(record)
68
+ when Symbol
69
+ option_value = record.send(option_value)
70
+ end
71
+
72
+ option_value = parse_as_number(option_value)
73
+
74
+ unless value.send(CHECKS[option], option_value)
75
+ record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def is_number?(raw_value)
84
+ !parse_as_number(raw_value).nil?
85
+ rescue ArgumentError, TypeError
86
+ false
87
+ end
88
+
89
+ def parse_as_number(raw_value)
90
+ if raw_value.is_a?(Float)
91
+ raw_value.to_d
92
+ elsif raw_value.is_a?(Numeric)
93
+ raw_value
94
+ elsif is_integer?(raw_value)
95
+ raw_value.to_i
96
+ elsif !is_hexadecimal_literal?(raw_value)
97
+ Kernel.Float(raw_value).to_d
98
+ end
99
+ end
100
+
101
+ def is_integer?(raw_value)
102
+ INTEGER_REGEX.match?(raw_value.to_s)
103
+ end
104
+
105
+ def is_hexadecimal_literal?(raw_value)
106
+ /\A0[xX]/.match?(raw_value.to_s)
107
+ end
108
+
109
+ def filtered_options(value)
110
+ filtered = options.except(*RESERVED_OPTIONS)
111
+ filtered[:value] = value
112
+ filtered
113
+ end
114
+
115
+ def allow_only_integer?(record)
116
+ case options[:only_integer]
117
+ when Symbol
118
+ record.send(options[:only_integer])
119
+ when Proc
120
+ options[:only_integer].call(record)
121
+ else
122
+ options[:only_integer]
123
+ end
124
+ end
125
+
126
+ def record_attribute_changed_in_place?(record, attr_name)
127
+ record.respond_to?(:attribute_changed_in_place?) &&
128
+ record.attribute_changed_in_place?(attr_name.to_s)
129
+ end
130
+ end
131
+
132
+ module HelperMethods
133
+ # Validates whether the value of the specified attribute is numeric by
134
+ # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
135
+ # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
136
+ # (if <tt>only_integer</tt> is set to +true+).
137
+ #
138
+ # class Person < ActiveRecord::Base
139
+ # validates_numericality_of :value, on: :create
140
+ # end
141
+ #
142
+ # Configuration options:
143
+ # * <tt>:message</tt> - A custom error message (default is: "is not a number").
144
+ # * <tt>:only_integer</tt> - Specifies whether the value has to be an
145
+ # integer, e.g. an integral value (default is +false+).
146
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
147
+ # +false+). Notice that for Integer and Float columns empty strings are
148
+ # converted to +nil+.
149
+ # * <tt>:greater_than</tt> - Specifies the value must be greater than the
150
+ # supplied value.
151
+ # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
152
+ # greater than or equal the supplied value.
153
+ # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
154
+ # value.
155
+ # * <tt>:less_than</tt> - Specifies the value must be less than the
156
+ # supplied value.
157
+ # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
158
+ # than or equal the supplied value.
159
+ # * <tt>:other_than</tt> - Specifies the value must be other than the
160
+ # supplied value.
161
+ # * <tt>:odd</tt> - Specifies the value must be an odd number.
162
+ # * <tt>:even</tt> - Specifies the value must be an even number.
163
+ #
164
+ # There is also a list of default options supported by every validator:
165
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
166
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
167
+ #
168
+ # The following checks can also be supplied with a proc or a symbol which
169
+ # corresponds to a method:
170
+ #
171
+ # * <tt>:greater_than</tt>
172
+ # * <tt>:greater_than_or_equal_to</tt>
173
+ # * <tt>:equal_to</tt>
174
+ # * <tt>:less_than</tt>
175
+ # * <tt>:less_than_or_equal_to</tt>
176
+ # * <tt>:only_integer</tt>
177
+ #
178
+ # For example:
179
+ #
180
+ # class Person < ActiveRecord::Base
181
+ # validates_numericality_of :width, less_than: ->(person) { person.height }
182
+ # validates_numericality_of :width, greater_than: :minimum_weight
183
+ # end
184
+ def validates_numericality_of(*attr_names)
185
+ validates_with NumericalityValidator, _merge_attributes(attr_names)
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ class PresenceValidator < EachValidator # :nodoc:
6
+ def validate_each(record, attr_name, value)
7
+ record.errors.add(attr_name, :blank, options) if value.blank?
8
+ end
9
+ end
10
+
11
+ module HelperMethods
12
+ # Validates that the specified attributes are not blank (as defined by
13
+ # Object#blank?). Happens by default on save.
14
+ #
15
+ # class Person < ActiveRecord::Base
16
+ # validates_presence_of :first_name
17
+ # end
18
+ #
19
+ # The first_name attribute must be in the object and it cannot be blank.
20
+ #
21
+ # If you want to validate the presence of a boolean field (where the real
22
+ # values are +true+ and +false+), you will want to use
23
+ # <tt>validates_inclusion_of :field_name, in: [true, false]</tt>.
24
+ #
25
+ # This is due to the way Object#blank? handles boolean values:
26
+ # <tt>false.blank? # => true</tt>.
27
+ #
28
+ # Configuration options:
29
+ # * <tt>:message</tt> - A custom error message (default is: "can't be blank").
30
+ #
31
+ # There is also a list of default options supported by every validator:
32
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
33
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
34
+ def validates_presence_of(*attr_names)
35
+ validates_with PresenceValidator, _merge_attributes(attr_names)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/slice"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ module ClassMethods
8
+ # This method is a shortcut to all default validators and any custom
9
+ # validator classes ending in 'Validator'. Note that Rails default
10
+ # validators can be overridden inside specific classes by creating
11
+ # custom validator classes in their place such as PresenceValidator.
12
+ #
13
+ # Examples of using the default rails validators:
14
+ #
15
+ # validates :terms, acceptance: true
16
+ # validates :password, confirmation: true
17
+ # validates :username, exclusion: { in: %w(admin superuser) }
18
+ # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
19
+ # validates :age, inclusion: { in: 0..9 }
20
+ # validates :first_name, length: { maximum: 30 }
21
+ # validates :age, numericality: true
22
+ # validates :username, presence: true
23
+ #
24
+ # The power of the +validates+ method comes when using custom validators
25
+ # and default validators in one call for a given attribute.
26
+ #
27
+ # class EmailValidator < ActiveModel::EachValidator
28
+ # def validate_each(record, attribute, value)
29
+ # record.errors.add attribute, (options[:message] || "is not an email") unless
30
+ # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
31
+ # end
32
+ # end
33
+ #
34
+ # class Person
35
+ # include ActiveModel::Validations
36
+ # attr_accessor :name, :email
37
+ #
38
+ # validates :name, presence: true, length: { maximum: 100 }
39
+ # validates :email, presence: true, email: true
40
+ # end
41
+ #
42
+ # Validator classes may also exist within the class being validated
43
+ # allowing custom modules of validators to be included as needed.
44
+ #
45
+ # class Film
46
+ # include ActiveModel::Validations
47
+ #
48
+ # class TitleValidator < ActiveModel::EachValidator
49
+ # def validate_each(record, attribute, value)
50
+ # record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
51
+ # end
52
+ # end
53
+ #
54
+ # validates :name, title: true
55
+ # end
56
+ #
57
+ # Additionally validator classes may be in another namespace and still
58
+ # used within any class.
59
+ #
60
+ # validates :name, :'film/title' => true
61
+ #
62
+ # The validators hash can also handle regular expressions, ranges, arrays
63
+ # and strings in shortcut form.
64
+ #
65
+ # validates :email, format: /@/
66
+ # validates :role, inclusion: %(admin contributor)
67
+ # validates :password, length: 6..20
68
+ #
69
+ # When using shortcut form, ranges and arrays are passed to your
70
+ # validator's initializer as <tt>options[:in]</tt> while other types
71
+ # including regular expressions and strings are passed as <tt>options[:with]</tt>.
72
+ #
73
+ # There is also a list of options that could be used along with validators:
74
+ #
75
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
76
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
77
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
78
+ # <tt>on: :custom_validation_context</tt> or
79
+ # <tt>on: [:create, :custom_validation_context]</tt>)
80
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
81
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
82
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
83
+ # proc or string should return or evaluate to a +true+ or +false+ value.
84
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
85
+ # if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
86
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
87
+ # method, proc or string should return or evaluate to a +true+ or
88
+ # +false+ value.
89
+ # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
90
+ # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
91
+ # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
92
+ # will raise ActiveModel::StrictValidationFailed instead of adding the error.
93
+ # <tt>:strict</tt> option can also be set to any other exception.
94
+ #
95
+ # Example:
96
+ #
97
+ # validates :password, presence: true, confirmation: true, if: :password_required?
98
+ # validates :token, length: 24, strict: TokenLengthException
99
+ #
100
+ #
101
+ # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
102
+ # and +:message+ can be given to one specific validator, as a hash:
103
+ #
104
+ # validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true
105
+ def validates(*attributes)
106
+ defaults = attributes.extract_options!.dup
107
+ validations = defaults.slice!(*_validates_default_keys)
108
+
109
+ raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
110
+ raise ArgumentError, "You need to supply at least one validation" if validations.empty?
111
+
112
+ defaults[:attributes] = attributes
113
+
114
+ validations.each do |key, options|
115
+ next unless options
116
+ key = "#{key.to_s.camelize}Validator"
117
+
118
+ begin
119
+ validator = key.include?("::") ? key.constantize : const_get(key)
120
+ rescue NameError
121
+ raise ArgumentError, "Unknown validator: '#{key}'"
122
+ end
123
+
124
+ validates_with(validator, defaults.merge(_parse_validates_options(options)))
125
+ end
126
+ end
127
+
128
+ # This method is used to define validations that cannot be corrected by end
129
+ # users and are considered exceptional. So each validator defined with bang
130
+ # or <tt>:strict</tt> option set to <tt>true</tt> will always raise
131
+ # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error
132
+ # when validation fails. See <tt>validates</tt> for more information about
133
+ # the validation itself.
134
+ #
135
+ # class Person
136
+ # include ActiveModel::Validations
137
+ #
138
+ # attr_accessor :name
139
+ # validates! :name, presence: true
140
+ # end
141
+ #
142
+ # person = Person.new
143
+ # person.name = ''
144
+ # person.valid?
145
+ # # => ActiveModel::StrictValidationFailed: Name can't be blank
146
+ def validates!(*attributes)
147
+ options = attributes.extract_options!
148
+ options[:strict] = true
149
+ validates(*(attributes << options))
150
+ end
151
+
152
+ private
153
+
154
+ # When creating custom validators, it might be useful to be able to specify
155
+ # additional default keys. This can be done by overwriting this method.
156
+ def _validates_default_keys
157
+ [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
158
+ end
159
+
160
+ def _parse_validates_options(options)
161
+ case options
162
+ when TrueClass
163
+ {}
164
+ when Hash
165
+ options
166
+ when Range, Array
167
+ { in: options }
168
+ else
169
+ { with: options }
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end