Validations for non-ActiveRecord Model Objects
Posted by Peter Donald Fri, 02 Dec 2005 11:48:00 GMT
Rails provides support for validating form input if the form is backed by an ActiveRecord. The application I am currently working on has a form that has a large number of input parameters but is not persisted to the database. I still wanted to use the ActiveRecord Validations as they make my life easier but I did not know if there was an simple way to do this.
Initially I created a dummy table in the database with just an id field and made my model object sub-class ActiveRecord. I could then use the validations with all the fields I had defined using attr_accessor. This looked something like;
class Search < ActiveRecord::Base
attr_accessor :user_name, :email, :locator
validates_length_of :user_name,
:within => 6..20,
:too_long => "pick a shorter name",
:too_short => "pick a longer name"
validates_format_of :email,
:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
validates_numericality_of :locator
...
endIn my controller I created the Search object in the same way that I created all the other model objects but I never called save. Instead I called the valid? method to check whether the model passed all the validations. If the model is not valid the @search.errors object is populated with all the errors.
class NavigatorController < ApplicationController
def search
@search = Search.new(params[:search])
if @search.valid?
...
end
end
...
endOf course this left a bad taste in my mouth as it is a seriously ugly hack that requires an empty table in the database just to get form validation working. So I began to look at what I needed to do to implement an ActiveForm object. I was not looking forward to this task as I had read on the rails mailing list that the Validations were intermingled with ActiveRecord::Base and difficult to untangle.
This could not be further from the truth. The first thing I did was create a new ActiveForm class and include ActiveRecord::Validations. This caused a few errors as the ActiveRecord::Validations class attempts to call alias_method for methods that do not exist in ActiveForm. I implement these methods (save and update_attribute) so that they raise a NotImplementedError exception. Then I attempt to call the valid? method but it calls the new_record? method which I implement to return true. To view the errors in the view using the standard helper methods I need to implement the human_attribute_name method. These changes seem to get basic validations working.
The only validations that are not working are validates_uniqueness_of and validates_numericality_of. validates_uniqueness_of is not expected to work as it accesses the database so I just make it raise a NotImplementedError exception. validates_numericality_of
does not work as it relies on a method named ”#{attr_name}_before_type_cast” for each attribute named “attr_name”. This is an artifact of the type coercion that ActiveRecord performs on input parameters. ActiveRecord will convert an input parameter from a string to an integer if the underlying database record stores the field as an integer. As this does not occur with ActiveForm I just duplicated the method and replaced ”#{attr_name}_before_type_cast” with ”#{attr_name}”.
The only functionality that ActiveForm was missing was the ability to create a model object from a hash. As ActiveForm does not need to do any type coercion this is as simple as
def initialize(attributes = nil)
if attributes
attributes.each do |key,value|
send(key.to_s + '=', value)
end
end
yield self if block_given?
endAt this stage ActiveForm is in a usable state and it took less than 20 minutes. It only took that long because I needed to restart webrick for each change (not to mention the fact that I had never looked at ActiveRecord before). Isn’t ruby/rails great?
To get this working grab the active_form.rb file and place it in the app/models directory. You can then make your model objects extend ActiveForm and use them like regular ActiveRecord objects.
I cleaned up a few warts of ActiveForm like overriding methods you should not be calling (save!, save_with_validation, create!, validate_on_create, validate_on_update). I hope to get motivated enough to send a patch that enables this style of functionality in the core once edge rails is working for me again.
Update:
It seems there is already a HowTo on the rails wiki that describes a similar technique. However rather than duplicating validates_numericality_of they handle the calls to ”#{attr_name}_before_type_cast” by implementing a method_missing method which I incorporated to cleanup my code.
Update on 12th Dec 2005
Today I decided that I needed to add reloading of ActiveForm subclasses and this is done with the following code chunk.
require 'dispatcher'
class Dispatcher
class << self
if ! method_defined?(:form_original_reset_application!)
alias :form_original_reset_application! :reset_application!
def reset_application!
form_original_reset_application!
Dependencies.remove_subclasses_for(ActiveForm) if defined?(ActiveForm)
end
end
end
endDownload: active_form.rb
Update on 1st March 2006
Available as a plugin at
Followed your link from the Ruby on Rails mailing list digest. You shoud add a link to your code on the rubyonrails wiki if you haven’t done so already. Very handy – thanks Peter!
Martin
I went to create a wiki page on this and found that there is already a HowTo so I may just update that page with some more details.
Peter
There is also an article Validate your forms with a table-less model by Rick Olson that may be useful
Peter