homeASCIIcasts

211: Validations in Rails 3 

(view original Railscast)

Other translations: Es It

In this episode we’ll take a look at the new additions to validations in Rails 3. Before we do that, however, we’ll upgrade Rails to the newest beta version, beta 3.

Upgrading Rails 3

We can install this new version in the same way as we’ve installed previous versions by running:

gem install rails --pre

After installing the new beta you might have some difficulty running the rails command and see an error message that looks like this.

$ rails store
/Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `require': no such file to load -- rails/cli (LoadError)
	from /Users/eifion/.rvm/gems/ruby-head/gems/rails-3.0.0.beta3/bin/rails:1:in `<top (required)>'
	from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `load'
	from /Users/eifion/.rvm/gems/ruby-head/bin/rails:19:in `<main>'

This error is caused by backwards-compatibility errors with the previous beta versions. To fix this you can run

gem cleanup

which will remove older versions of installed gems. Now you can run the rails command successfully and create the application we’re going to use for in this episode.

rails store

The New Features

Now that we have a Rails application to work with we can start to look at some of the new validation features. We’ll start by generating a scaffold for a User model with two attributes: a name and an email address.

rails g scaffold user name:string email:string

Then we’ll migrate the database in the usual way.

rake db:migrate

If we take a look at the form partial that the scaffolding generated we’ll see the code that displays the validation errors inline.

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
  <div id="errorExplanation">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

This is different from what you’d normally expect to see as the code to display the error information is fairly verbose in the view code. The reason for this is that in the latest release the error_messages_for helper method is no longer available and neither is

<%= f.error_messages %>

These methods have been extracted out into a plugin called dynamic_form. Installing this plugin will bring these methods back so that we could carry on as before. These methods have been moved in to a plugin for a reason, however, so it’s not recommended that you do this.

The reason that the methods have been removed is that the display of the error messages often needs to be customized and doing this through the old methods was a little bit cumbersome and not as flexible as having the error message HTML inline as we have now. Having the HTML to hand in the views means that we can change the display of the error messages however we like.

Of course having the error message code inline on each model’s form can lead to duplication in the code. To reduce this we can extract the error code out into a partial. So that we can use the partial across a number of models we’ll put it into a shared folder and call it _error_messages.html.erb, passing in the target object which in this case is our user.

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <%= render "shared/error_messages", :target => @user %>
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

In the new partial we’ll paste the code we cut from the form, changing the @user variable to target so that it references the object that was passed in. We’ll also change the error message to make it more generic.

/app/views/shared/_error_messages.html.erb

<% if target.errors.any? %>
<div id="errorExplanation">
  <h2><%= pluralize(target.errors.count, "error") %> prohibited this record from being saved:</h2>
  <ul>
  <% target.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
  <% end %>
  </ul>
</div>
<% end %>

We can test out the new partial by adding validation to the email attribute of our User model.

/app/models/user.rb

class User < ActiveRecord::Base
  validates_presence_of :email
end

If we go to the new user form and submit it without entering anything into the textfields we’ll see the validation errors as usual.

The error message from our partial.

So, it’s fairly easy to move the error message code into a partial and keep things flexible. If we do want the old helper methods back though we can use a technique similar to that used by Ryan Bates in his Nifty Generators. This has an ErrorMessagesHelper module that includes an error_messages_for method along with an error_messages method for form builders. These are simplified versions of the methods that Rails provided and are easy to customize to your needs so if you want to use them then you can add that helper method into your application.

Validation Reflection

Another new feature in Rails 3 is the ability to reflect on validations. We can use this if, for example, we want to put an asterisk next to each field in the form that is required. Previously to do this we’d have to install an external plugin so that we could determine which validations were in place for the model on which the form is based (in this case our User model), but now this is included in Rails it’s much easier to add an indicator to each field.

We can demonstrate this most easily in the console. We can call validators against a model to get an array of the validations that apply to that model.

ruby-head > User.validators
 => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]

We only have one validation against our User model so we’ll just see that one validator returned which, as the validation is a validates_presence_of, will be a PresenceValidator.

We can return the validators for just one attribute by using the validators_on method.

ruby-head > User.validators_on(:email)
 => [#<ActiveModel::Validations::PresenceValidator:0x00000100badfc8 @attributes=[:email], @options={}>]

This will return the same validator as our one validator acts against the email attribute. If we call the method against the name attribute then we’ll see an empty array returned.

ruby-head > User.validators_on(:name)
 => []

Ideally we’d want to perform this check in the form builder so that the asterisk is added automatically in the most efficient manner but doing that would be out of the scope of this episode so instead we’ll add a custom helper method that we’ll call mark_required. This method will take two parameters: a model and an attribute for that model. For the email attribute of our User model this method will be called like this:

<%= mark_required(@user, :email) %>

We can create the mark_required method in the ApplicationHelper file.

/app/helpers/application_helper.rb

module ApplicationHelper
  def mark_required(object, attribute)
    "*" if object.class.validators_on(attribute).map(&:class).include? ActiveModel::Validations::PresenceValidator
  end
end

The method returns a string containing an asterisk if the model’s validators include a PresenceValidator. Now that we have this method we can apply it to each field in our form.

/app/views/users/_form.html.erb

<%= form_for(@user) do |f| %>
  <%= render "shared/error_messages", :target => @user %>
  <div class="field">
    <%= f.label :name %><%= mark_required(@user, :name) %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :email %><%= mark_required(@user, :email) %><br /> 
    <%= f.text_field :email %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

As we mentioned before this isn’t the best way to do this as we have to manually add a call to the method against each field but extending FormBuilder to handle this shouldn’t be too difficult.

If we reload the user form now we’ll see the asterisk against the email field but not against the name.

Required fields now show an asterisk.

Model Layer Validations

So far we’ve only covered validations in the view layer but there are some excellent additions to validations in the model layer too. For more details about this it’s worth reading this article on Mikel Lindsaar’s blog. One important difference is that the validations have been moved out of ActiveRecord into the new ActiveModel. This makes it easier to add validation to non-ActiveRecord objects and this is something we’ll look at in greater detail in a future episode.

What we’re going to focus on here is making the validations in our User model more efficient. We’ve added a couple of validations to the model so that it now looks like this:

/app/models/user.rb

class User < ActiveRecord::Base
  validates_presence_of :email
  validates_uniqueness_of :email
  validates_format_of :email, :with => /^[\w\d]+$/ :on => :create, :message => "is invalid"
end

In Rails 3 it’s possible to call a validates method and pass it a hash of attributes to define the validations instead of defining each validation separately.

/app/models/user.rb

class User < ActiveRecord::Base
  validates :email, 
            :presence => true, 
            :uniqueness => true, 
            :format => { :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i }
end

In the User model we’re still validating that the field has a value and that the value is unique. For validating the format there are a number of options we can pass so we use a secondary hash to define those.

We can supply any number of validations for an attribute with a single command. While this is useful it can become cumbersome if there are a large number of validations but for most situations it works nicely.

Writing a Custom Validator

Let’s take a look now at how we can make the :format option more concise and clean it up a little. We often want to validate email addresses and having the same long regular expression in each validator is a little ugly and introduces repetition into the code. We can extract this out into a separate validation by creating a new class in our application’s /lib directory. We’ll call the file email_format_validator.rb.

/lib/email_format_validator.rb

class EmailFormatValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
      object.errors[attribute] << (options[:message] || "is not formatted properly")
    end
  end
end

The EmailFormatValidator class inherits from ActiveModel::EachValidator. We have to define one method in the class, validate_each, that takes three parameters called object, attribute and value. The method then checks that the value matches the regular expression we’re using to validate an email address and if not it will add the attribute to the objects errors.

We can use this technique to define any kind of validation we like. For example in the blog post we mentioned earlier a much more complex set of validations is used to validate an email address. Now that we have our custom validator we can update the validator in the User model to use it.

/app/models/user.rb

class User < ActiveRecord::Base
  validates :email, :presence => true, :uniqueness => true, :email_format => true
end

Having an email_format key in the validates hash means that the validator will look for a class called email_format_validator and passes the validation behaviour into the custom class that we just wrote.

If we try to create a new user now and enter an invalid email address we’ll see the expected error message.

Our custom error class showing an error.

Being able to create custom validators like this allows us to tidy up the validators in the models and makes it easy to remove duplications across validations.