Summary
Take the following:
class Person < ApplicationRecord
validate :custom_validation
private
def custom_validation
errors.add(:i_dont_exist, :presence)
end
end
Then:
person = Person.new
person.valid?
The problem
When valid?
is called, methods are executed down to generate_message
, which is called on the Error
class to build the validation messages.
generate_message
tries to fetch the attribute value for the attribute key provided to errors.add
by calling read_attribute_for_validation
on the base object being validated in case the attribute key passed is not :base
.
activemodel/lib/active_model/error.rb
module ActiveModel
...
class Error
...
def self.generate_message(attribute, type, base, options) # :nodoc:
...
value = (attribute != :base ? base.read_attribute_for_validation(attribute) : nil)
...
end
...
end
end
The read_attribute_for_validation
is currently just an alias to send
.
activemodel/lib/active_model/validations.rb
module ActiveModel
module Validations
extend ActiveSupport::Concern
...
module ClassMethods
...
alias :read_attribute_for_validation :send
end
end
end
Currently if a non-existent attribute key is passed to errors.add
, a NoMethodError
is raised during the generate_message
call (raised by send
under the hood), which does not hint at the fact that there’s a validation being run for an attribute that is not there.
The proposal
By overriding read_attribute_for_validation
as follows and checking if the object being validated does not respond to a given attribute being validated it’s possible to raise an error that actually points the developer to what’s going on.
activemodel/lib/active_model/validations.rb
module ActiveModel
module Validations
extend ActiveSupport::Concern
...
module ClassMethods
...
def read_attribute_for_validation(attr)
raise AbsentAttributeError.new(self, attr) unless self.respond_to?(attr)
send(attr)
end
end
end
...
class AbsentAttributeError < StandardError
attr_reader :record, :attribute
def initialize(record, attribute)
@attribute = attribute
super("Attribute #{attribute} does not exist for #{record.class}.")
end
end
end