Validations for non-ActiveRecord Model Objects

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

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

Of 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?
  end

At 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
end

Update 12th of May, 2010

The plugin is now available on GitHub. See the GitHub project page