Rails composed_of validation
ActiveRecord allows you to declaratively write validations (e.g. validates_presence_of) in any ActiveRecord::Base models. In addition, the errors will be stored in the @model#errors collection and be used by various view helpers and methods like FormBuilder, error_messages_for, and error_message_on, wrapping a nice error CSS around the offending html form elements. But when you have a normal domain model object that is non-ActiveRecord::Base such as a composed_of type Value Object, you will not have access to these declarative validation magic methods. Today let me try to elaborate on how to use ActiveRecord::Validation methods in your composed_of objects in Rails 2.0.
(The following entry revolves around the assumption that you are using f.fields_for to create and assign value object domain models onto your ActiveRecord::Base models. Using this approach eliminates most value object creation code in your controllers, achieving "Skinny Controllers". To learn about, visit, check out the Rails composed_of &block conversion blog entry.)
Using the same example in the previous entry, let's include our ActiveRecord::Validation module into our Money class like such:
class Money
include ActiveRecord:Validations
attr_reader :balance, :currency
validates_presence_of :currency
validates_inclusion_of :currency, :in => ['USD', 'EUR'], :if => :currency?
validates_presence_of :balance
validates_numericality_of :balance, :if => :balance?
def initialize(balance, currency)
@balance, @currency = balance, currency
@errors = ActiveRecord::Errors.new self
end
def new_record?
true
end
def currency?
!@currency.blank?
end
def balance?
!@balance.blank?
end
def self.human_attribute_name(attr)
attr.humanize
end
def balance_before_type_cast
@balance
end
end
You will notice a couple things. One, I have to define the #new_record? method. This method is defined on all ActiveRecord::Base objects, but since our PORO object is not a record per se, we just stub it out. Also, we need to store a collection @errors of type ActiveRecord::Errors.
Depending on what validation routine you will end up using in Money, you may have to stub out different methods. For example, I am showing error messages with error_messages_for (more on this later), and it requires stubbing out self.human_attribute_name (as of Rails 2.0, but no longer needed in future Rails). Using validates_numericality_of requires me to stub out balance_before_type_cast. Also, the validation :if conditions requires me to add the question-mark methods balance? and currency?. Remember, this approach does not give you all the validation magic. For example, validates_uniqueness_of will not work because it assumes too much about your object being a normal AR model and needs access to a database. But in practice, your Value Objects should not need such validations, and in most cases they contain only simple one-off validations, and provide simple functionalities such as formatting like this and this.
After all these, let's see our ActiveRecord Book class.
class Book < ActiveRecord::Bases
composed_of :balance, :class_name => 'Money', :mapping => [%w(balance balance), %w(currency currency)] do |params|
Money.new params[:balance], params[:currency]
end
validates_presence_of :name
validates_associated :balance
end
The composed_of conversion block remains. You will notice the validates_associated :balance line as well. This tells your book instances that they should not be persisted should there be any balance validations failing, just like any normal validations you would write. By default, any failing balance validation will add an error message 'Balance is invalid' in your @book#errors collection. If you want to suppress that message from showing up, you can pass in option :message => nil.
So, to put it all together, here is the view and the controller code:
Name:
Balance:
Currency:
New book
To show error messages from multiple objects on the view, I am using the view helper method error_messages_for(*args). The :object option actually allows you to pass an array of objects (c'mon, you should know this trick about ActiveRecord by now. If not, check it out here and here).
def create
@book = Book.new params[:book]
if @book.save
flash[:notice] = 'Book was successfully created.'
redirect_to @book
else
render :action => "new"
end
end
Again, a skinny, thin, sexy-looking controller action.