eco-friendly
metaprogramming
class Event < Doodle has Date end
Doodle uses kind_of? to test the value, so kind can be set to any module (e.g. Enumerable), class or superclass that makes sense for your application.
If you now try to initialize Event#date with something that isn't a Date:
event = Event.new(:date => "Hello")
you'll get a Doodle::ValidationError exception:
# ~> -:9: Event.date must be a kind of Date - got String("Hello") (Doodle::ValidationError)
To specify a validation on an attribute, use must inside the attribute's definition block:
class Event < Doodle has :start_date, :kind => Date do must "be >= today" do |value| value >= Date.today end end end event = Event :start_date => Date.today event.start_date = Date.parse('2001-01-01') # ~> -:14: Event.start_date must be >= today - got Date(#<Date: 4903821/2,0,2299161>) (Doodle::ValidationError)
The must block should return true if the value is valid, false otherwise. A failed validation will raise a Doodle::ValidationError exception.
Attribute validations happen before the instance variable is changed.
You can also specify validations for the object as a whole:
class Event < Doodle has :start_date, :kind => Date do must "be >= today" do |value| value >= Date.today end end has :end_date, :kind => Date do default { start_date } end must "have end_date >= start_date" do end_date >= start_date end end event = Event :start_date => Date.today event.end_date = Date.parse('2001-01-01') # ~> -:21: Event must have end_date >= start_date (Doodle::ValidationError)
Note that you don't need to project the value into the block but you can if you like - its value is the object instance itself.
Object level validations occur after all instance variables have been set.
Sometimes you need to update attributes in a way that would temporarily cause the object state to be invalid.
The following class definition has two object level validations defined with must:
class Dude < Doodle # the attribute has an ~attribute~ validation, i.e. it must be a # String has :name, :kind => String has :cool, :default => false # whereas this is an ~object~ level validation must "be cool if name contains 'Dude'" do !(name =~ /Dude/ && !cool) end must "not be cool if name does not contains 'Dude'" do !(cool && name !~ /Dude/) end end # ~> -:2: uninitialized constant Doodle (NameError)
If you try to simply update an attribute in such a way as to cause the instance to be invalid, Doodle will throw an exception:
dude = Dude("The Dude", true) dude.name = "Bozo" # ~> -:7: Dude must not be cool if name does not contains 'Dude' (Doodle::ValidationError)
Using object.doodle.update, you can temporarily set attributes to values which would invalidate the object:
dude.doodle.update do name "Bozo" name "The Dude" end dude # => #<Dude:0x5840bc @name="The Dude", @cool=true> dude.doodle.update do cool false name "Bozo" end dude # => #<Dude:0x5840bc @name="Bozo", @cool=false>
Values set in the block will override values set in the argument list:
dude.doodle.update :cool => false do cool true name "The Dude" end dude # => #<Dude:0x5840bc @name="The Dude", @cool=true>
Of course, if the object is invalid at the end of the block, Doodle will raise an exception:
dude.doodle.update do name "Bozo" cool true end # ~> -:7: Dude must not be cool if name does not contains 'Dude' (Doodle::ValidationError)
You can't escape individual attribute validations:
res = Doodle::Utils.try { dude.doodle.update do name 123 name "Dude" end } res # => #<Doodle::ValidationError: Dude.name must be a kind of String - got Fixnum(123)> # the attribute is still valid, even after capturing the exception dude # => #<Dude:0x5840bc @name="The Dude", @cool=true>
i.e. Doodle will not allow you to set an individual attribute to an invalid value for that attribute, even temporarily.
However if you capture the exception, update will not prevent you from invalidating the whole object:
res = Doodle::Utils.try { dude.doodle.update do name "Jeff" end } res # => #<Doodle::ValidationError: Dude must not be cool if name does not contains 'Dude'> dude # => #<Dude:0x5840bc @name="Jeff", @cool=true>