Friday, April 27, 2007

Ruby: Validatable 1.2.2 released

The Validatable gem received a fair amount of attention in the past few weeks. Thanks to Ali Aghareza, Jason Miller, Xavier Shay, and Anonymous Z for their contributions.

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 MortgageApplication
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
As 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.

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 Person
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
The logic option is required.

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 Person
include Validatable
validates_numericality_of :age
end
after_validate hook method
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 Person
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"
The 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_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 Person
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"
The 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.

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.

21 comments:

  1. Anonymous2:35 PM

    Out of interest, how do you recommend testing validations?

    As 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.

    ReplyDelete
  2. Anonymous2:46 PM

    Hello John,

    Short 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.

    ReplyDelete
  3. Hello Jay:

    Thanks 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.

    ReplyDelete
  4. Anonymous9:07 AM

    Hello Karthik,

    I 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

    ReplyDelete
  5. Hi Jay,

    Thanks 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.

    ReplyDelete
  6. Anonymous1:10 AM

    Dear Jay:

    Great 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.

    ReplyDelete
  7. Anonymous10:15 AM

    Hello Karthik and Venkat,

    I 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

    ReplyDelete
  8. Anonymous6:44 AM

    Dear Jay:

    Just 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.

    ReplyDelete
  9. Anonymous7:11 AM

    Venkat,
    Thanks for the patch, I'll include it today.
    Cheers, Jay

    ReplyDelete
  10. Anonymous9:50 PM

    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)

    Thanks again, Jay

    ReplyDelete
  11. Anonymous8:24 AM

    Jay

    just 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

    ReplyDelete
  12. Anonymous9:05 AM

    Hello Rupert,

    Actually, 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

    ReplyDelete
  13. Anonymous12:24 PM

    Jay

    Great 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

    ReplyDelete
  14. Anonymous12:52 PM

    Hello Rupert,

    I 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?

    ReplyDelete
  15. Anonymous2:46 PM

    Thanks for the updates/fixes - will be downloading after I've finished scribbling this...

    Initial 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

    ReplyDelete
  16. Anonymous7:26 AM

    Hello Rupert,

    I'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

    ReplyDelete
  17. Anonymous4:37 AM

    Cheers Jay

    ...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

    ReplyDelete
  18. Anonymous9:31 AM

    Hello Rupert,

    Thanks 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

    ReplyDelete
  19. Anonymous10:35 AM

    Rupert, you might want to join the Validatable mailing list:

    http://rubyforge.org/mailman/listinfo/validatable-developer

    ReplyDelete
  20. I installed Validatable per the instructions in the RDoc

    gem 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?

    ReplyDelete
  21. 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

Note: Only a member of this blog may post a comment.