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