require "active_support/core_ext/module/anonymous" module ActiveModel # == Active \Model \Validator # # A simple base class that can be used along with # ActiveModel::Validations::ClassMethods.validates_with # # class Person # include ActiveModel::Validations # validates_with MyValidator # end # # class MyValidator < ActiveModel::Validator # def validate(record) # if some_complex_logic # record.errors[:base] = "This record is invalid" # end # end # # private # def some_complex_logic # # ... # end # end # # Any class that inherits from ActiveModel::Validator must implement a method # called +validate+ which accepts a +record+. # # class Person # include ActiveModel::Validations # validates_with MyValidator # end # # class MyValidator < ActiveModel::Validator # def validate(record) # record # => The person instance being validated # options # => Any non-standard options passed to validates_with # end # end # # To cause a validation error, you must add to the +record+'s errors directly # from within the validators message. # # class MyValidator < ActiveModel::Validator # def validate(record) # record.errors.add :base, "This is some custom error message" # record.errors.add :first_name, "This is some complex validation" # # etc... # end # end # # To add behavior to the initialize method, use the following signature: # # class MyValidator < ActiveModel::Validator # def initialize(options) # super # @my_custom_field = options[:field_name] || :first_name # end # end # # The easiest way to add custom validators for validating individual attributes # is with the convenient ActiveModel::EachValidator. # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) # record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value) # end # end # # This can now be used in combination with the +validates+ method # (see ActiveModel::Validations::ClassMethods.validates for more on this). # # class Person # include ActiveModel::Validations # attr_accessor :title # # validates :title, presence: true # end # # Validator may also define a +setup+ instance method which will get called # with the class that using that validator as its argument. This can be # useful when there are prerequisites such as an +attr_accessor+ being present. # # class MyValidator < ActiveModel::Validator # def setup(klass) # klass.send :attr_accessor, :custom_attribute # end # end # # This setup method is only called when used with validation macros or the # class level validates_with method. class Validator attr_reader :options # Returns the kind of the validator. # # PresenceValidator.kind # => :presence # UniquenessValidator.kind # => :uniqueness def self.kind @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? end # Accepts options that will be made available through the +options+ reader. def initialize(options) @options = options.freeze end # Return the kind for this validator. # # PresenceValidator.new.kind # => :presence # UniquenessValidator.new.kind # => :uniqueness  def kind self.class.kind end # Override this method in subclasses with validation logic, adding errors # to the records +errors+ array where necessary. def validate(record) raise NotImplementedError, "Subclasses must implement a validate(record) method." end end # +EachValidator+ is a validator which iterates through the attributes given # in the options hash invoking the validate_each method passing in the # record, attribute and value. # # All Active Model validations are built on top of this validator. class EachValidator < Validator #:nodoc: attr_reader :attributes # Returns a new validator instance. All options will be available via the # +options+ reader, however the :attributes option will be removed # and instead be made available through the +attributes+ reader. def initialize(options) @attributes = Array(options.delete(:attributes)) raise ArgumentError, ":attributes cannot be blank" if @attributes.empty? super check_validity! end # 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. def validate(record) attributes.each do |attribute| value = record.read_attribute_for_validation(attribute) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) validate_each(record, attribute, value) end end # Override this method in subclasses with the validation logic, adding # errors to the records +errors+ array where necessary. def validate_each(record, attribute, value) raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method" end # Hook method that gets called by the initializer allowing verification # that the arguments supplied are valid. You could for example raise an # +ArgumentError+ when invalid options are supplied. def check_validity! end end # +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. class BlockValidator < EachValidator #:nodoc: def initialize(options, &block) @block = block super end private def validate_each(record, attribute, value) @block.call(record, attribute, value) end end end