activemodel 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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