Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created March 27, 2010 07:08
Show Gist options
  • Save mcmire/345789 to your computer and use it in GitHub Desktop.
Save mcmire/345789 to your computer and use it in GitHub Desktop.
Run a single validation on a single attribute
module ActiveRecord
class Base
VALIDATION_SYMBOLS = %w(
acceptance confirmation exclusion format inclusion length numericality presence size uniqueness
).inject({}) {|hash, name| hash["validates_#{name}_of"] = name; hash }
VALIDATION_METHODS = VALIDATION_SYMBOLS.invert
class << self
def validations
@validations ||= {}
end
# Override validates_each so that each validation routine is stored in a hash keyed by the name
# of the validation method and the name of the attribute being validated
# Every validation method uses this except validates_presence_of
def validates_each(*attrs, &block)
options = attrs.extract_options!.symbolize_keys
attrs = attrs.flatten
# Declare the validation.
validation_routine = lambda do |record, attr|
value = record.send(attr)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
block.call(record, attr, value)
end
attrs.each {|attr| (validations[attr.to_s] ||= {})[options[:name].to_s] = validation_routine }
send(validation_method(options[:on] || :save), options) do |record|
attrs.each {|attr| validation_routine.call(record, attr) }
end
end
# Override validates_presence_of (which is the only method that doesn't use validates_each) so that each
# validation routine is stored in a hash keyed by the name of the validation method and the name of the
# attribute being validated
def validates_presence_of(*attrs)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save, :name => "validates_presence_of" }
configuration.update(attrs.extract_options!)
validation_routine = lambda do |record, attr|
record.errors.add_on_blank(attr, configuration[:message])
end
attrs.each {|attr| (validations[attr.to_s] ||= {})[configuration[:name].to_s] = validation_routine }
# can't use validates_each here, because it cannot cope with nonexistent attributes,
# while errors.add_on_empty can
send(validation_method(configuration[:on]), configuration) do |record|
attrs.each {|attr| validation_routine.call(record, attr) }
end
end
# Intercept the other validation methods so that the name of the method is passed as a configuration option
# so we can use it when storing the validation routine in the 'validations' hash
(VALIDATION_SYMBOLS.keys - ['validates_presence_of']).each do |method|
class_eval <<-EOC, __FILE__, __LINE__
def #{method}_with_single_attr_validations(*attrs)
options = attrs.extract_options!.symbolize_keys
options[:name] = "#{method}"
#{method}_without_single_attr_validations(*(attrs << options))
end
alias_method_chain :#{method}, :single_attr_validations
EOC
end
# Intercept custom validation callbacks so that block is stored in 'validations' hash as well
# If a given method is a two-element array (e.g. [:permaname, :must_be_unique]) then the first
# element is assumed to be the attribute being validated and the second is the name of that validation.
%w(validate validate_on_create validate_on_update).each do |method|
class_eval <<-EOC, __FILE__, __LINE__
def #{method}_with_single_attr_validations(*methods, &block)
methods.map! do |method|
if method.is_a?(Array)
attr, validation_name = method
method = "validate_"+method.map(&:to_s).join("_").to_sym
(validations[attr.to_s] ||= {})[validation_name.to_s] = method.to_s
end
method
end
#{method}_without_single_attr_validations(*methods, &block)
end
alias_method_chain :#{method}, :single_attr_validations
EOC
end
end
# Run a single validation on a single attribute
def valid_by?(name, attr)
name = name.to_s
attr = attr.to_s
num_errors = self.errors_on(attr).size
unless validations_for_attr = self.class.validations[attr]
raise "No validations have been set on the '#{attr}' attribute!"
end
#unless validation_method = [VALIDATION_METHODS[name], "#{attr}_#{name}", name].first {|method| respond_to?(method) || Entry.respond_to?(method) }
# raise "#{self.class} does not respond to '#{validation_method}'!"
#end
validation_method = VALIDATION_METHODS[name] || name
unless block_or_symbol = validations_for_attr[validation_method]
raise "No validation '#{validation_method}' set on #{attr}!"
end
(block_or_symbol.is_a?(Symbol) || block_or_symbol.is_a?(String)) ? send(block_or_symbol) : block_or_symbol.call(self, attr)
# no real way to tell whether the validation succeeded or failed except for this
self.errors_on(attr).size == num_errors
end
# This lets you say e.g. entry.title_valid_by?(:presence)
def method_missing_with_single_attr_validations(name, *args)
name = name.to_s
if name =~ /^(.+)_valid_by\?$/
raise "Must pass an attribute to a _valid_by? method" unless args.size == 1
valid_by?(args.first, $1)
else
method_missing_without_single_attr_validations(name, *args)
end
end
alias_method_chain :method_missing, :single_attr_validations
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment