Validation Groups
On my previous project we found that our object.valid? method needed to depend on the role that an object was currently playing. This led to the introduction of groups.
Validation groups can be used to validate an object when it can be valid in various states. For example a mortgage application may be valid for saving (saving a partial application), but that same mortgage application would not be valid for underwriting. In our example a application can be saved as long as a Social Security Number is present; however, an application can not be underwritten unless the name attribute contains a value.
class MortgageApplicationAs you can see, you can use an array if the validation needs to be part of various groups. However, if the validation only applies to one group you can simply use a symbol for the group name.
include Validatable
validates_presence_of :ssn, :groups => [:saving, :underwriting]
validates_presence_of :name, :groups => :underwriting
attr_accessor :name, :ssn
end
application = MortgageApplication.new
application.ssn = 377990118
application.valid_for_saving? #=> true
application.valid_for_underwriting? #=> false
The inspiration for adding this functionality came from the ContextualValidation entry by Martin Fowler.
validates_true_for
The validates_true_for method was added to allow for custom validations.
The validates_true_for method can be used to specify a proc, and add an error unless the evaluation of that proc returns true.
class PersonThe logic option is required.
include Validatable
validates_true_for :first_name, :logic => lambda { first_name == 'Book' }
attr_accessor :first_name
end
person = Person.new
person.valid? #=> false
person.first_name = 'Book'
person.valid? #=> true
validates_numericality_of
This release also adds the validates_numericality_of method. The validates_numericality_of method takes all of the standard parameters that the other validations take: message, times, level, if, group.
Validates that the specified attribute is numeric.
class Personafter_validate hook method
include Validatable
validates_numericality_of :age
end
Another new feature of this release is the after_validate hook. This feature allows you to manipulate the instance after a validation has been run. For example, perhaps you are happy with the default messages; however, you also want the attribute to be appended to the message. The following code uses that example and shows how the after_validate hook can be used to achieve the desired behavior.
class PersonThe after_validate hook yields the result of the validation being run, the instance the validation was run on, and the attribute that was validated.
include Validatable
validates_presence_of :name
attr_accessor :name
end
class ValidatesPresenceOf
after_validate do |result, instance, attribute|
instance.errors.add("#{attribute} can't be blank") unless result
end
end
person = Person.new
person.valid? #=> false
person.errors.on(:name) #=> "name can't be blank"
include_validations_for takes options
The include_validations_for method was changed to accept options. The currently supported options for include_validations_for are :map and :if.
class PersonThe person attribute will be validated. If person is invalid the errors will be added to the PersonPresenter errors collection. The :map option is used to map errors on attributes of person to attributes of PersonPresenter. Also, the :if option ensures that the person attribute will only be validated if it is not nil.
include Validatable
validates_presence_of :name
attr_accessor :name
end
class PersonPresenter
include Validatable
include_validations_for :person, :map => { :name => :namen },
:if => lambda { not person.nil? }
attr_accessor :person
def initialize(person)
@person = person
end
end
presenter = PersonPresenter.new(Person.new)
presenter.valid? #=> false
presenter.errors.on(:namen) #=> "can't be blank"
validates_confirmation_of
The validates_confirmation_of method now takes the :case_sensitive option. If :case_sensitive is set to false, the confirmation will validate the strings based a case insensitive comparison.
validates_length_of
The validates_length_of method now takes the :is option. If the :is option is specified, the length will be required to be equal to the value given to :is.
Bugs
The comparison in validates_format_of was changed to call to_s on the object before execution. The new version allows you to validate the format of any object that implements to_s.
Out of interest, how do you recommend testing validations?
ReplyDeleteAs in, when testing, one makes the assumption that library code works, so you wouldn't want to test that there is a validation error if age isn't numeric as that wouldn't be within the scope of your application... but you would still want some way to make sure that validation is there in the correct form.
Hello John,
ReplyDeleteShort Answer: I haven't found a method that I like enough to recommend.
Long Answer: I've written about this topic in the past at this location.
Hello Jay:
ReplyDeleteThanks for writing this and sharing it. I am trying to use Validatable 1.3 in AR models.
I am getting a "wrong number of arguments (1 for 0)" when using validates_presence_of.
It would be very useful to use it as drop in replacement for AR's built in validations.
Any ideas?
Thanks,
Karthik.
Hello Karthik,
ReplyDeleteI haven't tried to make this work, but I'll make some time to try it out this weekend. Look for something by Monday.
Cheers, Jay
Hi Jay,
ReplyDeleteThanks for your quick response.
In rails, Errors are displayed in the View by highlighting the input component as well as displaying the error summary on top.
When we tried to use Validatable plugin, we encountered errors that the "count" and "full_messages" methods were missing in Validatable::Errors class.
Please look at http://pastie.caboo.se/64472
for the same. Please note that the full_messages method has dependency to the Active Support that it may not be able to run in a standalone ruby environment.
If you think this implementation is ok, I can provide a diff patch to the Validatable project in ruby forge.
Thanks,
Karthik.
Dear Jay:
ReplyDeleteGreat that you will consider looking into adding drop in replacement support for AR validations. I am Karthik's (above poster) colleague.
To add to Karthik's last comment, the the full_message method he is referring to uses the "humanize" method from ActiveSupport. It would be just as easy to copy the humanize code from AS to eliminate that dependency.
Let us know if you feel this is a good addition. If you like we shall submit a diff based patch to the tracker of Validatable project for the above mentioned change.
Thanks,
Venkat.
Hello Karthik and Venkat,
ReplyDeleteI would very much appreciate a patch, thanks for offering. Please do check out the latest version of the project (@rubyforge.org/var/svn/validatable/trunk). I've (just now) committed a patch from someone else and it contains changes to the Errors class, and I'd prefer not to have any conflicts.
Cheers, Jay
Dear Jay:
ReplyDeleteJust to let you know. The patch we were talking about in the above posts is already available in the tracker of the same rubyforge project.
Cheeers,
Venkat.
Venkat,
ReplyDeleteThanks for the patch, I'll include it today.
Cheers, Jay
The patch has been included and I've added you to the contributors list. I also released a new version of the Validatable that includes your changes. (1.3.2)
ReplyDeleteThanks again, Jay
Jay
ReplyDeletejust discovered your site - wonderful!!!
have been playing with Validatable and am not sure if I've done something wrong or misunderstood the concept behind validation groups...
I am using it with an active record class where I am wanting to allow an attribute to be missing under some situations and present for others. However when I run the valid? method on an object it is returning false for validations which have been declared as a member of a group. i.e. in terms of the example you give if I run application.valid? I'm getting false as it's failing validates_presence_of_name :name.
I was sortof expecting that valid? would ignore validations that are members of groups, but from what I've found this doesn't appear to be the case (unless I've botched up my code).
This got me thinking that a handy addition to the gem would be some 'save_for' methods too, so if you wish to save the mortgage application you could do:
application.save_for_saving (which would work as there is a ssn)
application.save_for_underwriting (which wouldn't work as there isn't a name)
application.save (which would work as there are no validations specified outside groups)
To make things even DRYer, perhaps an additional parameter could be used when dealing with groups to specify whether the validation should be run when the valid? (or save) method is called or only when the valid_for_role? (or save_for_role) method is called. Something like :include_in_default_validation => true
I'm not sure if this makes sense or not as I'm newish to ruby/rails so your code looks like wondrous magic!!
Cheers
Rupert
Hello Rupert,
ReplyDeleteActually, what you are saying makes perfect sense. I've been running into the same exact problem on 2 different projects I've been involved with. I like your idea with the save methods, but I'm hesitant to make valid? run only validations that aren't part of a group. Ultimately, that might be the right answer, I just need some time to think on it since it will change the behavior of the public interface.
For now, I'd suggest removing ActiveRecord validations if your are planning on replacing them with Validatable validations. That will pull the call to valid? out of any save methods and you can manually check valid.
model.save if model.valid_for_saving?
Conceptually, I do like the idea of generating save methods, but I'd like something with better readability.
Is model.save_if_valid_for_saving and model.save_if_valid_for_underwriting too verbose?
I think I prefer that as it's more intent expressive.
Cheers, Jay
Jay
ReplyDeleteGreat stuff - thanks! I do prefer the readability of the
model.save_if_valid_for_underwriting
syntax - it makes it very clear what's going on - and I don't think it's overly verbose - nice one :)
I still think it would be handy if there was a way to prevent valid? from running validations that are part of a group. So you don't break the public interface, could you not add another parameter to the validation so you could have:
validates_presence_of :name, :groups => :underwriting, :only_for_groups => true
with :only_for_groups defaulting to false (and hence the current behavior) if not specified.
Although I'm not entirely sure how good an idea this is either ...I need to play around with 'things I might like to do' a bit more.
On another topic, I found a problem when using include_validations_for and :groups in the same class - I was getting:
wrong number of arguments (1 for 0)
validatable_class_methods.rb:202:in `valid?'.
I found I could get the error to go away by changing the valid? method in
validatable_instance_methods to:
# call-seq: valid?
#
# Returns true if no errors were added otherwise false.
def valid?(*groups)
valid_for_group?(*groups)
end
I've no idea if this change is a good idea or not as I don't really understand what's going on and I don't know how to submit patches even if I did know if it's a good fix (***newbie alert***) - hence posting it here!
I'll have more of a think about valid? and groups and let you know if I come up with any useful thoughts.
Cheers again and many thanks
Rupert
Hello Rupert,
ReplyDeleteI spent some time this morning working on a few various bugs. I fixed the one you reported and a few more. The new version (1.4.0) is now available on rubyforge.
On saving: After I thought about this a bit, save is part of ActiveRecord and not really about validations. My first instinct was not to include it based on that; however, I then thought about this instead:
http://pastie.textmate.org/67256
The idea being that you can hook any method in by declaring it. Given the linked example, if you call save it will validate for saving first and if you call process it will validate for underwriting first.
How does that sound?
As far as the valid? method, I'm beginning to lean towards your original thought, making valid? only call validations that have no group specified. The flag would work also, but I'm thinking that most people who use groups are going to put their validations in groups and never need the valid? method.
Thoughts?
Thanks for the updates/fixes - will be downloading after I've finished scribbling this...
ReplyDeleteInitial thoughts...
saving:
I'm not sure that I can do what I was hoping to be able to do to do with the 'pastie code' (assuming I've not misunderstood it of course, which is always a strong possibility). The background is I have a Member model (this is for a local club which people can join for a nominal fee). A site administrator can create a new member, but I don't want them to have to bother to fill in all the members details - they should be able to just fill in the member's name and email address then save the model. So I'm wanting to be able to save the Member while validating the name and email (and allow the postal address and phone number attributes to be blank) when the admin initially creates the object. I was thinking that if I assigned a group online_application to check just the email and name I could use:
if @member.save_if_valid_for_online_application
...success stuff
else
...failure stuff
end
in the controller's create method. If the administrator has to add the complete set of member details manually (say the member isn't online - yes there is one!) then I can use (in a different controller method)
if @member.save_if_valid_for_completed_details
...success stuff
else
...failure stuff
end
where the completed_details group would validate name, email and postal address/phone numbers. I would also use the same save method when a member completed their own details after the admin has filled in their initial details .
Without the save_for methods this would have to be done (I think) using something along the lines of:
if @member.valid_for_online_application?
@member.save(false) # have to use false to disable validation when saving afaik
...success stuff
else
...failure stuff
end
...ditto for 'completed_details'. This just seems less elegant.
valid? method:
I'd certainly be a happy bunny with valid? ignoring all validations with groups specified. Of course there's bound to be someone who wants it to work the other way, but personally I think you're right that most people who use groups are going to put their validations in groups and not use the valid? method.
While I remember, one other thing that would be handy is if include_validations_for also supported the :groups syntax - assuming it's not too messy/complicated to achieve!?!
ooh, and thanks hoogely for your entry 'Autoloading Gems in Vendor' - I'd spent quite a bit of unfruitful time trying to work out how to find out what was going wrong when I hit the bug with include_validations_for and :groups until I found your posting!
Thanks again for everything Jay - your blog's really coming in handy as I start trying to use rails (and ruby) properly specially as I'm starting to grapple with basing everything on tests.
Cheers
Rupert
Hello Rupert,
ReplyDeleteI've been looking for a way to generalize hooking validatable into any method, since save has as much to do with validations as any other method. Unfortunately, I haven't come up with anything that seems to make sense.
On valid?, I'll make that change in the near future, just busy this week. :/
Also, I'll look into adding :groups to child relationships.
Cheers, Jay
Cheers Jay
ReplyDelete...been busy too :) btw, I stumbled accross ActiveSpec on Luke Redpath's blog the other day, which seems to do something similar but in a totally different way. I've not played with it, only had time to give it a quick read-through so can't comment on it really but thought I'd let you know of it's existance in case you've not seen it:
ActiveSpec
Thanks again for all - cheers
Rupert
Hello Rupert,
ReplyDeleteThanks for the link; unfortunately, it doesn't look like there's been any activity that project since Sept. :/
As far as adding :groups to include_validations_for, it already passes on the :groups option to the child.
For example: http://pastie.textmate.org/69096
I'll work on the valid? change today, probably. :)
Cheers, Jay
Rupert, you might want to join the Validatable mailing list:
ReplyDeletehttp://rubyforge.org/mailman/listinfo/validatable-developer
I installed Validatable per the instructions in the RDoc
ReplyDeletegem install validatable
(Mac OSX 10.5 Leopard)
and I was getting
>> c = Contact.new
NameError: uninitialized constant Contact::Validatable
If I added
require 'validatable'
to my environment.rb, everything worked.
Was I doing something wrong? Should this be added to the instructions?
Hello, I'm getting familiar with Validatable. I want to change all the ActiveRecord Validations on my application to Validatable. But I have a question. Since ActiveRecord also handles associations, how can use Validatable and keep working the models relationships such as has_one, belongs_to, has_many, etc.
ReplyDelete