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

Line #7 actually works for me on Rails 3.0.12 and Ruby 1.9.2; what versions are you using?

Loading development environment (Rails 3.0.12)
irb(main):001:0> class Feed < ActiveRecord::Base; end
=> nil
irb(main):002:0> f = Feed.new
=> #<Feed id: nil, url: nil, title: nil, updated_at: nil>
irb(main):003:0> f.valid?
=> true
irb(main):004:0> f.singleton_class.validates_presence_of :title
=> [ActiveModel::Validations::PresenceValidator]
irb(main):005:0> f.valid?
=> false
irb(main):006:0> f.errors
=> {:title=>["can't be blank"]}

@h-lame
Copy link

h-lame commented Mar 29, 2012

I think the problem of adding them to the singleton class is that the data that contains the validation callbacks is not unique to the singleton, so adding the validtion ends up adding it to the class anyway.

For what it's worth, I use something like this in some tests:

module AlwaysFail
  class Validator
    def self.before_validation(record)
      record.errors.add(:base, :invalid)
    end
  end

  def make_model_always_fail(model)
    setup do
      model.before_validation AlwaysFail::Validator
    end

    teardown do
      model._validation_callbacks.delete_if { |c| c.raw_filter == AlwaysFail::Validator }
      model.__define_runner(:validation)
    end
  end
end

So that for the duration of a specific testcase that model will always fail to save, but I do have to "reset" the validation (note this is slightly different for rails 3.2)

@mudge
Copy link

mudge commented Mar 29, 2012

Trying to dig into this some more to see if @h-lame is right about the validations being defined on the class regardless of using singleton_class:

Loading development environment (Rails 3.0.12)
irb(main):001:0> class Feed < ActiveRecord::Base; end
=> nil
irb(main):002:0> f = Feed.new
f.valid?=> #<Feed id: nil, url: nil, title: nil, updated_at: nil>
irb(main):003:0> f.valid?
=> true

So without any validation whatsoever, the singleton class looks like so:

irb(main):004:0> f.singleton_class._validators
=> {}

When you add a validation however, this hash will be populated:

irb(main):005:0> f.singleton_class.validates_presence_of :title
=> [ActiveModel::Validations::PresenceValidator]
irb(main):006:0> f.valid?
=> false
irb(main):007:0> f.singleton_class._validators
=> {:title=>[#<ActiveModel::Validations::PresenceValidator:0x00000103bf3868 @attributes=[:title], @options={}>]}

Creating a new instance:

irb(main):008:0> f2 = Feed.new
=> #<Feed id: nil, url: nil, title: nil, updated_at: nil>
irb(main):009:0> f2.valid?
=> true

And let's see if the validator is there (it shouldn't be seeing as our new instance passed all validations):

irb(main):010:0> f2.singleton_class._validators
=> {:title=>[#<ActiveModel::Validations::PresenceValidator:0x00000103bf3868 @attributes=[:title], @options={}>]}

This is a tad mind-boggling.

irb(main):017:0> f2.singleton_class._validators[:title].object_id
=> 2178915360
irb(main):018:0> f.singleton_class._validators[:title].object_id
=> 2178915360

@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