aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/validations/validates.rb
blob: e28e7e92194e53ffb8676fa7bff66b6a1f81c7e3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# frozen_string_literal: true

require "active_support/core_ext/hash/slice"

module ActiveModel
  module Validations
    module ClassMethods
      # This method is a shortcut to all default validators and any custom
      # validator classes ending in 'Validator'. Note that Rails default
      # validators can be overridden inside specific classes by creating
      # custom validator classes in their place such as PresenceValidator.
      #
      # Examples of using the default rails validators:
      #
      #   validates :terms, acceptance: true
      #   validates :password, confirmation: true
      #   validates :username, exclusion: { in: %w(admin superuser) }
      #   validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
      #   validates :age, inclusion: { in: 0..9 }
      #   validates :first_name, length: { maximum: 30 }
      #   validates :age, numericality: true
      #   validates :username, presence: true
      #
      # The power of the +validates+ method comes when using custom validators
      # and default validators in one call for a given attribute.
      #
      #   class EmailValidator < ActiveModel::EachValidator
      #     def validate_each(record, attribute, value)
      #       record.errors.add attribute, (options[:message] || "is not an email") unless
      #         value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      #     end
      #   end
      #
      #   class Person
      #     include ActiveModel::Validations
      #     attr_accessor :name, :email
      #
      #     validates :name, presence: true, length: { maximum: 100 }
      #     validates :email, presence: true, email: true
      #   end
      #
      # Validator classes may also exist within the class being validated
      # allowing custom modules of validators to be included as needed.
      #
      #   class Film
      #     include ActiveModel::Validations
      #
      #     class TitleValidator < ActiveModel::EachValidator
      #       def validate_each(record, attribute, value)
      #         record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
      #       end
      #     end
      #
      #     validates :name, title: true
      #   end
      #
      # Additionally validator classes may be in another namespace and still
      # used within any class.
      #
      #   validates :name, :'film/title' => true
      #
      # The validators hash can also handle regular expressions, ranges, arrays
      # and strings in shortcut form.
      #
      #   validates :email, format: /@/
      #   validates :gender, inclusion: %w(male female)
      #   validates :password, length: 6..20
      #
      # When using shortcut form, ranges and arrays are passed to your
      # validator's initializer as <tt>options[:in]</tt> while other types
      # including regular expressions and strings are passed as <tt>options[:with]</tt>.
      #
      # There is also a list of options that could be used along with validators:
      #
      # * <tt>:on</tt> - Specifies the contexts where this validation is active.
      #   Runs in all validation contexts by default +nil+. You can pass a symbol
      #   or an array of symbols. (e.g. <tt>on: :create</tt> or
      #   <tt>on: :custom_validation_context</tt> or
      #   <tt>on: [:create, :custom_validation_context]</tt>)
      # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
      #   if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
      #   or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
      #   proc or string should return or evaluate to a +true+ or +false+ value.
      # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
      #   if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
      #   or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
      #   method, proc or string should return or evaluate to a +true+ or
      #   +false+ value.
      # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
      # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
      # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
      #   will raise ActiveModel::StrictValidationFailed instead of adding the error.
      #   <tt>:strict</tt> option can also be set to any other exception.
      #
      # Example:
      #
      #   validates :password, presence: true, confirmation: true, if: :password_required?
      #   validates :token, length: 24, strict: TokenLengthException
      #
      #
      # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
      # and +:message+ can be given to one specific validator, as a hash:
      #
      #   validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true
      def validates(*attributes)
        defaults = attributes.extract_options!.dup
        validations = defaults.slice!(*_validates_default_keys)

        raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
        raise ArgumentError, "You need to supply at least one validation" if validations.empty?

        defaults[:attributes] = attributes

        validations.each do |key, options|
          next unless options
          key = "#{key.to_s.camelize}Validator"

          begin
            validator = key.include?("::".freeze) ? key.constantize : const_get(key)
          rescue NameError
            raise ArgumentError, "Unknown validator: '#{key}'"
          end

          validates_with(validator, defaults.merge(_parse_validates_options(options)))
        end
      end

      # This method is used to define validations that cannot be corrected by end
      # users and are considered exceptional. So each validator defined with bang
      # or <tt>:strict</tt> option set to <tt>true</tt> will always raise
      # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error
      # when validation fails. See <tt>validates</tt> for more information about
      # the validation itself.
      #
      #   class Person
      #     include ActiveModel::Validations
      #
      #     attr_accessor :name
      #     validates! :name, presence: true
      #   end
      #
      #   person = Person.new
      #   person.name = ''
      #   person.valid?
      #   # => ActiveModel::StrictValidationFailed: Name can't be blank
      def validates!(*attributes)
        options = attributes.extract_options!
        options[:strict] = true
        validates(*(attributes << options))
      end

    private

      # When creating custom validators, it might be useful to be able to specify
      # additional default keys. This can be done by overwriting this method.
      def _validates_default_keys
        [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
      end

      def _parse_validates_options(options)
        case options
        when TrueClass
          {}
        when Hash
          options
        when Range, Array
          { in: options }
        else
          { with: options }
        end
      end
    end
  end
end