-
-
Save thechrisoshow/2236521 to your computer and use it in GitHub Desktop.
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? |
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)
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
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.
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
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...
I would like to do the exact same thing. Any news on this?
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
Another solution would be using a Form Object and defining the validations depending on the context where they're used.
class class User
before_validation :add_instance_validations
private
def add_instance_validations
singleton_class.class_eval { validates :name, presence: true }
end
end
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
wrote an article specially answering this topic: http://www.eq8.eu/blogs/22-different-ways-how-to-do-rails-validations :)
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
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.
Line #7 actually works for me on Rails 3.0.12 and Ruby 1.9.2; what versions are you using?