Skip to content

Instantly share code, notes, and snippets.

@thechrisoshow
Created March 29, 2012 11:56
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thechrisoshow/2236521 to your computer and use it in GitHub Desktop.
Save thechrisoshow/2236521 to your computer and use it in GitHub Desktop.
How to add validations to a specific instance of an active record object?
class Banana < ActiveRecord::Base; end
banana = Banana.new
banana.valid? #=> true
banana.singleton_class.validates_presence_of :name
banana.valid? #=> true - why did the validation not work?
banana.class.validates_presence_of :name
banana.valid? #=> false - as we'd expect...but now...
new_banana = Banana.new
new_banana.valid? #=> false - because the previous call soiled the Banana class with it's validation
# So how does one apply validations to the eigenclass of an ActiveRecord object?
# Or am I misunderstanding what .singleton_class is?
@mudge
Copy link

mudge commented Mar 29, 2012

Ah, here's the key difference (as @h-lame's teardown hinted at):

irb(main):033:0> f.singleton_class._validate_callbacks
=> [#<ActiveSupport::Callbacks::Callback:0x00000103bf35c0 @klass=#<Class:#<Feed:0x00000103cbef40>>(id: integer, url: string, title: string, updated_at: datetime), @kind=:before, @chain=[...], @per_key={:if=>[], :unless=>[]}, @options={:if=>[], :unless=>[]}, @raw_filter=#<ActiveModel::Validations::PresenceValidator:0x00000103bf3868 @attributes=[:title], @options={}>, @filter="_callback_before_13", @compiled_options=[], @callback_id=14>]
irb(main):034:0> f2.singleton_class._validate_callbacks
=> []

Both have the validator, but only the first has the callback to use it.

@thechrisoshow
Copy link
Author

Woops! I thought I had posted a reply.

@mudge I should've mentioned, Rails 2.3.14, Ruby 1.8.7
Looks like you CAN do what I want in Rails 3 - sorry to send you off down a deprecated Rabbit Hole

@jesseclark
Copy link

Is there a trick to accomplishing this in Rails 3.2?

(rdb:1) @nmp.singleton_class._validators
{:child_new_medical_profiles=>[#<Mongoid::Validations::AssociatedValidator:0x0000010443f378 @attributes=[:child_new_medical_profiles], @options={}>], :conditions=>[#<Mongoid::Validations::AssociatedValidator:0x00000105965b58 @attributes=[:conditions], @options={}>]}

(rdb:1) @nmp.singleton_class._validate_callbacks.map &:filter
["_callback_before_139", "_callback_before_141", :validate_not_more_than_one_condition, :inches, :feet]

(rdb:1) @nmp.singleton_class.validates_presence_of :number
[Mongoid::Validations::PresenceValidator]

(rdb:1) @nmp.singleton_class._validators
{:child_new_medical_profiles=>[#<Mongoid::Validations::AssociatedValidator:0x0000010443f378 @attributes=[:child_new_medical_profiles], @options={}>], :conditions=>[#<Mongoid::Validations::AssociatedValidator:0x00000105965b58 @attributes=[:conditions], @options={}>], :number=>[#<Mongoid::Validations::PresenceValidator:0x00000102bdbd68 @attributes=[:number], @options={}>]}

(rdb:1) @nmp.singleton_class._validate_callbacks.map &:filter
["_callback_before_139", "_callback_before_141", :validate_not_more_than_one_condition, :inches, :feet, "_callback_before_359"]
(rdb:1) 

After calling validates_presence_of of the singleton_class I can see that it adds the validator and creates a new callback, "_callback_before_359". If I inspect that callback it is a huge long callback chain that does seem to include a callback with @raw_filter equal to my new PresenceValidator.

However, the validator is not called:

(rdb:1) @nmp.number = nil
nil

(rdb:1) @nmp.valid?
true

Any help would be greatly appreciated...

@kaligrafy
Copy link

I would like to do the exact same thing. Any news on this?

@nicosuria
Copy link

You may be better off just using #alias_method_chain

# Extending a User instance with this decorator will add a validation that the :old_password attribute is valid
class User
  module PasswordProtection

    def self.extended(user)
      class << user
        attr_writer :old_password
        alias_method_chain :valid?, :password_protection
      end
    end

    def valid_with_password_protection?
      valid_without_password_protection?
      validate_old_password
      errors.empty?
    end

    private

    def validate_old_password
      unless self.valid_password?(@old_password)
        errors.add :old_password, "is invalid"
      end
    end
  end
end

@bilus
Copy link

bilus commented Apr 16, 2014

Another solution would be using a Form Object and defining the validations depending on the context where they're used.

@divineforest
Copy link

class class User
  before_validation :add_instance_validations

  private

    def add_instance_validations
      singleton_class.class_eval { validates :name, presence: true }
    end
end

@DVG
Copy link

DVG commented Mar 5, 2015

I actually just blogged bout this, if anyone is still in need of a solution

http://dvg.github.io/2015/03/05/apply-activemodel-validations-by-policy.html

@equivalent
Copy link

wrote an article specially answering this topic: http://www.eq8.eu/blogs/22-different-ways-how-to-do-rails-validations :)

@trcarden
Copy link

A method that is directly built into Rails: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations/with.rb#L115-L123

       class Person
         include ActiveModel::Validations

         validate :instance_validations, on: :create

         def instance_validations
           validates_with MyValidator, MyOtherValidator
         end
       end

@danielminsalesforce
Copy link

A method that is directly built into Rails: https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations/with.rb#L115-L123

       class Person
         include ActiveModel::Validations

         validate :instance_validations, on: :create

         def instance_validations
           validates_with MyValidator, MyOtherValidator
         end
       end

This solution is not corresponding with the initial subdmision. What the author need is to be able to "inject" as ad-hoc a validator without affecting to the Base class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment