I'm having some difficulty with a validation. I'm working with four models - Animal
Cat
Dog
and Breed
. Animal
is an abstract class - yes, I realize Rails doesn't have those, but that's how I'm using it; it's never directly initialized, but a lot of common logic exists inside it for all its children to use. I'm also using STI, so Animal
, Cat
and Dog
use the same table.
Breed
meanwhile is going to be in a has_and_belongs_to_many relationship with Animal
, but I want to restrict what breeds are available based on the model being used. So Breed has an attribute animal_type
which is a string, that corresponds to the child class. The validation for this attribute is what I'm having trouble with. I'm trying to ensure you can only create a breed for a type of animal that exists.
Lastly, for organizational purposes, Animal
and Breed
are in an Animals
module, to get them out of the way.
Here's how my code looks:
module Animals
class Animals::Animal < ActiveRecord::Base
end
class Animals::Breed < ActiveRecord::Base
validates :animal_type, inclusion: { in: Animal.descendants.map {|d| d.name}
end
end
class Cat < Animals::Animal
end
class Dog < Animals::Animal
end
What should happen is the validator should be generating an array of the names of all child models of Animal
, then it should compare the string animal_type
and return true if the same value exists anywhere in the array.
I'm testing this with the console, and its failing despite me manually ensuring the values are equal. I'm not sure if it's caused by lazy loading (the first commands I run when I start the console are Cat.connection
and Dog.connection
so they appear in the Animal.descendants
array), or by the module (since Animal
and Breed
are in the same module, I'm pretty sure I've got the reference correct).
I'm stuck, and I'm not an advanced enough user to know where to go next.
Looks like class loading will affect you, as long as the Animals
module is all loaded in one go. At the point that the code inside the Breed
class runs, there are no descendants of Animal
yet, so the array passed to the validation is empty.
The inclusion validation allows you to specify the list with a lambda, ie
validates :animal_type, inclusion: { in: -> { Animal.descendants.map {|d| d.name}}
The lambda is called at the point where the validation is run, so you won't run into your current issue. You still need to ensure that the various descendant classes are loaded.
I poked around at your question for a bit. Best as I can tell, the inclusion validator doesn't like being passed the Animal.descendants.map {|d| d.name}
Animal.descendants.map {|d| d.name}
bit. So how about you just make a custom validation? Something like:
module Animals
class Animal < ActiveRecord::Base
end
class Breed < ActiveRecord::Base
validate :animal_type_is_a_descendant_class
private
def animal_type_is_a_descendant_class
if animal_type.nil?
errors.add(:animal_type, "can't be blank")
elsif !defined?(animal_type)
errors.add(:animal_type, "is not a valid class")
elsif !Animals::Animal.descendants.include?(animal_type.constantize)
errors.add(:animal_type, "is not a descendant")
end
end
end
end
class Cat < Animals::Animal
end
class Dog < Animals::Animal
end
class Horse
end
If I load that into console and then do:
b = Animals::Breed.new
b.animal_type = "Cat"
b.valid?
b.animal_type = "Dog"
b.valid?
b.animal_type = nil
b.valid?
b.errors
b.animal_type = "Horse"
b.valid?
b.errors
I get:
irb(main):344:0* b = Animals::Breed.new
=> #<Animals::Breed:0x65639a0>
irb(main):345:0> b.animal_type = "Cat"
=> "Cat"
irb(main):346:0> b.valid?
=> true
irb(main):347:0>
irb(main):348:0* b.animal_type = "Dog"
=> "Dog"
irb(main):349:0> b.valid?
=> true
irb(main):350:0>
irb(main):351:0* b.animal_type = nil
=> nil
irb(main):352:0> b.valid?
=> false
irb(main):353:0> b.errors
=> #<ActiveModel::Errors:0x60b9a30 @base=#<Animals::Breed:0x65639a0 @animal_type=nil, @validation_context=nil, @errors=#<ActiveModel::Errors:0x60b9a30 ...>>, @messages={:animal_type=>["can't be blank"]}>
irb(main):354:0>
irb(main):355:0* b.animal_type = "Horse"
=> "Horse"
irb(main):356:0> b.valid?
=> false
irb(main):357:0> b.errors
=> #<ActiveModel::Errors:0x60b9a30 @base=#<Animals::Breed:0x65639a0 @animal_type="Horse", @validation_context=nil, @errors=#<ActiveModel::Errors:0x60b9a30 ...>>, @messages={:animal_type=>["is not a descendant"]}>
irb(main):358:0>
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.