1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activemodel/lib/active_model/validator.rb
eileencodes 2c02bc0a47 Revert changes to validations from PR #18612
In order to fix issue #17621 we added a check to validations that
determined if a record should be validated. Based on the existing tests
and behavior we wanted we determined the best way to do that was by
checking if `!record.peristed? || record.changed? || record.marked_for_destruction?`

This change didn't make it into a release until now. When #23790 was
opened we realized that `valid?` and `invalid?` were broken and did not
work on persisted records because of the `!record.persisted?`.

While there is still a bug that #17621 brought up, this change was too
drastic and should not be a RC blocker. I will work on fixing this so
that we don't break `valid?` but also aren't validating parent records
through child records if that parent record is validate false. This
change removes the code changes to validate and the corresponding tests.
It adds tests for two of the bugs found since Rails 5 beta2 release.

Fixes #17621
2016-02-23 15:21:46 -05:00

182 lines
6.1 KiB
Ruby

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.add(: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
#
# Note that the validator is initialized only once for the whole application
# life cycle, and not on each validation run.
#
# The easiest way to add custom validators for validating individual attributes
# is with the convenient <tt>ActiveModel::EachValidator</tt>.
#
# 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 <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this).
#
# class Person
# include ActiveModel::Validations
# attr_accessor :title
#
# validates :title, presence: true, title: true
# end
#
# It can be useful to access the class that is using that validator when there are prerequisites such
# as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
# To setup your validator override the constructor.
#
# class MyValidator < ActiveModel::Validator
# def initialize(options={})
# super
# options[:class].send :attr_accessor, :custom_attribute
# end
# end
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.except(:class).freeze
end
# Returns 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 <tt>validate_each</tt> 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 <tt>:attributes</tt> 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