I noticed I could DRY up a lot of my def validate items by finding a better way to check if a numerical field was a positive number (when provided). I figured this would be a perfect candidate for extending the "validates_numericality_of" method. But then I had a play with extending it and got stuck on "how exactly do you extend a class method?"
I've successfully added new validations by reopening ActiveRecord::Base, but how do you open an exising method and add more bits on?
I tried alising the method... but the question became: how do you alias a method that is defined as self.validates_whatever?
This excellent post describes how to extend class methods by using base.class_eval and putting the alises into class << self
Note: you definitely need to double-alias your function (as in the class extension section below) or you will find yourself instantly spiralling into a "stack level too deep" exception the first time you call it.
Here's how to extend a validation in Rails (includes my extension to validates_numericality_of, and also my preivous validates_percentage method):
module MyNewValidations
# magic to allow us to override existing validations
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
class << self
alias old_numericality_of :validates_numericality_of unless method_defined?(:old_numericality_of)
alias validates_numericality_of :my_validates_numericality
end
end
end
module ClassMethods
# extends the "validates numericality of" validation to allow the option
# ":positive_only => true" or ":negative_only => true"
# This will validate to true only if the given number is positive (>= 0)
# or negative (<= 0) respectively
# Otherwise is behaves exactly as the standard validation
def my_validates_numericality(fields, args = {})
ret = old_numericality_of(fields, args) # first call standard numericality
pos = args[:positive_only] || false
neg = args[:negative_only] || false
if pos || neg
msg = args[:message] || "should be a #{pos ? 'positive' : 'negative'} number"
validates_each fields do |model, attr, val|
if (pos && val.to_f < 0) || (neg && val.to_f > 0)
model.errors.add attr, msg
ret = false
end
end
end
ret
end
# validates whether the given object is a percentage
# Can also take optional args which will get passed verbatim into the
# validation methods. Thus it's only really safe to use ":allow_nil" and
# ":message"
def validates_percentage(fields, args = {})
msg = args[:message] || "should be a percentage (0-100)"
validates_each fields do |model, attr, val|
pre_val = model.send("#{attr}_before_type_cast".to_sym)
unless val.nil? || !pre_val.is_a?(String) || pre_val =~ /^[-+]?\d+(\.\d*)?%?$/
model.errors.add(attr, msg)
end
end
args[:message] = msg
args[:in] = 0..100
validates_inclusion_of fields, args
end
end
end
class ActiveRecord::Base
# add in the extra validations created above
include MyNewValidations
#other stuff I'd defined goes here
end
No comments:
Post a Comment