validator.rb 6.2 KB
Newer Older
1 2
require "active_support/core_ext/module/anonymous"

3
module ActiveModel
4

5
  # == Active \Model \Validator
6 7
  #
  # A simple base class that can be used along with
A
Alexey Mahotkin 已提交
8
  # ActiveModel::Validations::ClassMethods.validates_with
9
  #
10 11
  #   class Person
  #     include ActiveModel::Validations
12 13 14
  #     validates_with MyValidator
  #   end
  #
15
  #   class MyValidator < ActiveModel::Validator
16
  #     def validate(record)
17
  #       if some_complex_logic
18
  #         record.errors.messages[:base] << "This record is invalid"
19 20 21 22 23 24 25 26 27
  #       end
  #     end
  #
  #     private
  #       def some_complex_logic
  #         # ...
  #       end
  #   end
  #
28
  # Any class that inherits from ActiveModel::Validator must implement a method
29
  # called +validate+ which accepts a +record+.
30
  #
31 32
  #   class Person
  #     include ActiveModel::Validations
33 34 35
  #     validates_with MyValidator
  #   end
  #
36
  #   class MyValidator < ActiveModel::Validator
37
  #     def validate(record)
38 39 40 41 42
  #       record # => The person instance being validated
  #       options # => Any non-standard options passed to validates_with
  #     end
  #   end
  #
43 44
  # To cause a validation error, you must add to the +record+'s errors directly
  # from within the validators message.
45
  #
46
  #   class MyValidator < ActiveModel::Validator
47
  #     def validate(record)
48 49
  #       record.errors.add :base, "This is some custom error message"
  #       record.errors.add :first_name, "This is some complex validation"
50 51 52 53 54 55
  #       # etc...
  #     end
  #   end
  #
  # To add behavior to the initialize method, use the following signature:
  #
56
  #   class MyValidator < ActiveModel::Validator
57
  #     def initialize(options)
58 59 60 61
  #       super
  #       @my_custom_field = options[:field_name] || :first_name
  #     end
  #   end
62
  #
63
  # Note that the validator is initialized only once for the whole application
64
  # life cycle, and not on each validation run.
65
  #
66
  # The easiest way to add custom validators for validating individual attributes
67
  # is with the convenient <tt>ActiveModel::EachValidator</tt>.
68
  #
69 70
  #   class TitleValidator < ActiveModel::EachValidator
  #     def validate_each(record, attribute, value)
71
  #       record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value)
72 73
  #     end
  #   end
74
  #
75
  # This can now be used in combination with the +validates+ method
76
  # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this).
77
  #
78 79 80
  #   class Person
  #     include ActiveModel::Validations
  #     attr_accessor :title
81
  #
82
  #     validates :title, presence: true, title: true
83
  #   end
84
  #
85
  # It can be useful to access the class that is using that validator when there are prerequisites such
R
Rafael Mendonça França 已提交
86
  # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
87
  # To setup your validator override the constructor.
88
  #
89
  #   class MyValidator < ActiveModel::Validator
90 91 92
  #     def initialize(options={})
  #       super
  #       options[:class].send :attr_accessor, :custom_attribute
93 94
  #     end
  #   end
95
  class Validator
96
    attr_reader :options
97

98
    # Returns the kind of the validator.
99
    #
100 101
    #   PresenceValidator.kind   # => :presence
    #   UniquenessValidator.kind # => :uniqueness
102 103 104 105
    def self.kind
      @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous?
    end

106
    # Accepts options that will be made available through the +options+ reader.
107
    def initialize(options = {})
108
      @options  = options.except(:class).freeze
109 110
    end

111
    # Returns the kind for this validator.
112
    #
113
    #   PresenceValidator.new.kind   # => :presence
114
    #   UniquenessValidator.new.kind # => :uniqueness
115 116 117 118
    def kind
      self.class.kind
    end

119 120
    # Override this method in subclasses with validation logic, adding errors
    # to the records +errors+ array where necessary.
121
    def validate(record)
122
      raise NotImplementedError, "Subclasses must implement a validate(record) method."
123 124 125
    end
  end

V
Vijay Dev 已提交
126 127
  # +EachValidator+ is a validator which iterates through the attributes given
  # in the options hash invoking the <tt>validate_each</tt> method passing in the
128 129
  # record, attribute and value.
  #
130
  # All \Active \Model validations are built on top of this validator.
131
  class EachValidator < Validator #:nodoc:
132
    attr_reader :attributes
133

134 135 136
    # Returns a new validator instance. All options will be available via the
    # +options+ reader, however the <tt>:attributes</tt> option will be removed
    # and instead be made available through the +attributes+ reader.
137
    def initialize(options)
138
      @attributes = Array(options.delete(:attributes))
139
      raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
140
      super
141
      check_validity!
142 143
    end

144 145 146
    # Performs validation on the supplied record. By default this will call
    # +validates_each+ to determine validity therefore subclasses should
    # override +validates_each+ with validation logic.
147 148
    def validate(record)
      attributes.each do |attribute|
149
        value = record.read_attribute_for_validation(attribute)
150 151 152 153 154
        next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
        validate_each(record, attribute, value)
      end
    end

155
    # Override this method in subclasses with the validation logic, adding
156
    # errors to the records +errors+ array where necessary.
157
    def validate_each(record, attribute, value)
158
      raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
159
    end
160

161 162
    # Hook method that gets called by the initializer allowing verification
    # that the arguments supplied are valid. You could for example raise an
V
Vijay Dev 已提交
163
    # +ArgumentError+ when invalid options are supplied.
164 165
    def check_validity!
    end
166 167 168 169

    def should_validate?(record) # :nodoc:
      !record.persisted? || record.changed? || record.marked_for_destruction?
    end
170
  end
171

V
Vijay Dev 已提交
172 173
  # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
  # and call this block for each attribute being validated. +validates_each+ uses this validator.
174
  class BlockValidator < EachValidator #:nodoc:
175 176 177 178 179
    def initialize(options, &block)
      @block = block
      super
    end

180 181
    private

182 183 184 185
    def validate_each(record, attribute, value)
      @block.call(record, attribute, value)
    end
  end
186
end