Friday, September 07, 2012

Reporting Input Errors from a Rails Model


There are several places outside of the standard validations where you might process user input in a Rails model and want to inform the user that they supplied bad data.  It is not immediately obvious though how you get useful error messages back to the user.  Rails gives your model an instance of ActiveRecord::Errors called errors, which is the standard way to return validation errors.  You have to think of any other user input checking as validation and somehow get error messages into the errors object.

Without further ado, from easiest to hardest:

Standard Validations

With a standard validation Rails handles adding an appropriate error message to errors for you.

class Thing < ActiveRecord::Base
  validates :name, presence: true
end

Custom Validations

To quote from the Rails guide to custom validation:

"You can also create methods that verify the state of your models and add messages to the errors collection when they are invalid."

class Thing < ActiveRecord::Base
  validate :custom_validator
  def custom_validator
    errors[:name] << 'This is a custom validation error on name'
  end
end

Before Validation Callback

Rails provides callbacks at a number of stages of model processing.

To quote from the Rails guide regarding halting execution in a callback:

"If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception."

"Raising an arbitrary exception may break code that expects save and its friends not to fail like that. The ActiveRecord::Rollback exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised."

It doesn't mention that before_validate callbacks have another option.

Specialized validation should normally go in a custom validator, but if for some reason you have to perform a check in a callback then any time you are processing user input you should do that in a before_validate callback.  Using before_validate allows you to use the standard errors object, which behaves just like it does in a custom validator.

class Thing < ActiveRecord::Base
  before_validation :bv
  def bv
    errors[:name] << 'This is a before validation error on name'
  end
end

Virtual Attribute

You can define virtual attributes in a model.  As shown in the RailsCast you can use custom validation for simple cases.  But some virtual attributes may be doing complex processing that causes you to discover errors in the user input inside the virtual attribute method.  Assignment methods are called before validation.  They're even called before the before_initialization callback for any virtual attributes you pass via new/create.

Apparently this is too early in the process to use the standard errors object, anything added to errors is ignored.  But since these methods execute before validation we can just save up any errors and insert them into the standard errors object during the validation stage.

class Thing < ActiveRecord::Base
  def virtual=(virtual_value)
    @virtual_errors = {name: ['Virtual is invalid']}
  end
  before_validation :bv
  def bv
    @virtual_errors.each do |k,v|
      v.each { |e| errors[k] << e }
    end
  end
end