# 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 # /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value) # 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 /\Athe/i.match?(value) # 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 :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your # validator's initializer as options[:in] while other types # including regular expressions and strings are passed as options[:with]. # # There is also a list of options that could be used along with validators: # # * :on - 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. on: :create or # on: :custom_validation_context or # on: [:create, :custom_validation_context]) # * :if - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. if: :allow_validation, # or if: Proc.new { |user| user.signup_step > 2 }). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * :unless - Specifies a method, proc or string to call to determine # if the validation should not occur (e.g. unless: :skip_validation, # or unless: Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a +true+ or # +false+ value. # * :allow_nil - Skip validation if the attribute is +nil+. # * :allow_blank - Skip validation if the attribute is blank. # * :strict - If the :strict option is set to true # will raise ActiveModel::StrictValidationFailed instead of adding the error. # :strict 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?("::") ? 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 :strict option set to true will always raise # ActiveModel::StrictValidationFailed instead of adding error # when validation fails. See validates 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