Benito Serna Tips and tools for Ruby on Rails developers

Building simpler models, using custom PORO validators

December 9, 2014

The validations of your ActiveRecord models look very convenient, because they are easy to use and have a pretty DSL. Use them is the “rails way” so you think that is what you must use. But…

When you start an application this validations look pretty harmless and you can think that even make tests for this validations is a waste of time. But with the time and more features one or two of the models of the application (maybe the User model) begins to grow and is no more that “simple” to understand what is happening.

At that moment you start to see code like:

user.save(validate: false)
class User < ActiveRecord::Base
  validates_format_of :password, with: password_format, on: [:registration]
end

This happen because some models are used in different contexts. For example in Rides, the model Trip can be created, published, edited (but not always), also can have seat requests, and a lot of other possibilities. Maybe your application have this kind of models that are used in a lot of places.

Now writing tests is not an option. The problem is that at this moment start to make tests is not that easy. There is a lot of coupling, the test will run slow and maybe the business rules that are implemented are not that clear.

If this is happening to you, you are not alone, even Discourse that is awesome have this problems.

class Topic < ActiveRecord::Base
  validates :title, :presence => true,
                    :topic_title_length => true,
                    :quality_title => { :unless => :private_message? },
                    :unique_among  => { :unless => Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) },
                                        :message => :has_already_been_used,
                                        :allow_blank => true,
                                        :case_sensitive => false,
                                        :collection => Proc.new{ Topic.listable_topics } }

  validates :category_id,
            :presence => true,
            :exclusion => {
              :in => Proc.new{[SiteSetting.uncategorized_category_id]}
            },
            :if => Proc.new { |t|
                   (t.new_record? || t.category_id_changed?) &&
                   !SiteSetting.allow_uncategorized_topics &&
                   (t.archetype.nil? || t.archetype == Archetype.default) &&
                   (!t.user_id || !t.user.staff?)
            }
end

But although this is common. I think is bad to have no idea what your code is doing.

Defining the interface of the validator

Imagine that you are working in an application to promote events. More specifically in the registration of events. And you have to validate the event has a name and that its date is in the future.

You will have a form with a field for the event name and other field for the start date. If you are using simple_form maybe it looks something like this:

<%= simple_form_for @event do |f| %>
  <%= f.input :name %>
  <%= f.input :starts_at %>
  <%= f.button :submit %>
<% end %>

And if you are using the rails conventions maybe the controller looks like this:

class EventsController < ApplicationController
  def new
    @event = Event.new
  end

  def create
    @event = Event.new(event_params)

    if @event.save
      redirect_to event_path(@event)
    else
      render :new
    end
  end
end

Now we will introduce the custom validator that will have the responsibility of identify the errors in the event and then send that errors to the model to be displayed in the view.

class EventsController < ApplicationController
  def new
    @event = Event.new
  end

  def create
    @event = Event.new(event_params)
    errors = EventRegistrationValidator.errors_for(@event)

    if errors.empty?
      @event.save
      redirect_to event_path(@event)
    else
      @event.add_errors(errors)
      render :new
    end
  end
end

Now we need to implement the method Event#add_errors

class Event < ActiveRecord::Base
  def add_errors(errors)
    errors.each { |error| self.errors.add(*error) }
  end
end

So Event#add_errors is waiting for a collection of errors with the form:

[[:name, :blank], [:starts_at, :must_be_in_the_future]]

This is an array of arrays, where each element are the parameters that describe an error in an ActiveRecord object. If we analyze this method in detail, what we are doing is.

event.errors.add(:name, :blank)
event.errors.add(:starts_at, :must_be_in_the_future)

The best part of this is that we can still use the default Rails translations using I18n

Implementing the validator

To implement the data structure that our validator needs to return first we will define the behavior with some tests.

describe 'Event registration validator' do
  describe 'validates the presence of name' do
    example do
      event = event_with(name: 'Benito')
      expect(errors_for(event, :name)).to be_empty
    end

     example do
      event = event_with(name: '')
      expect(errors_for(event, :name)).to eq [[:name, :blank]]
    end
  end

  describe 'validates it starts in the future' do
    example do
      event = event_with(starts_at: '')
      expect(errors_for(event, :starts_at)).to eq [[:starts_at, :blank]]
    end

    example do
      event = event_with(starts_at: 2.days.from_now)
      expect(errors_for(event, :starts_at)).to be_empty
    end

    example do
      event = event_with(starts_at: 1.second.ago)
      expect(errors_for(event, :starts_at))
        .to eq [[:starts_at, :must_be_in_the_future]]
    end
  end

  def event_with(attributes)
    TestEventRecord.new(attributes)
  end

  def errors_for(event, attribute)
    errors = EventRegistrationValidator.errors_for(event)
    errors.select { |error_attribute, _| error_attribute == attribute }
  end

  class TestEventRecord
    attr_reader :name, :starts

    def inititalize(name: nil, starts_at: nil)
      @name = name
      @starts_at = starts_at
    end
  end
end

Here you can see how instead of use the Event active record object, I used a little “mock” that in this case has the same behavior. Even thought the use this “mock” is no necessary, it can make you tests run in milliseconds instead of seconds.

Also you can see that the most important part is to define the expected behavior in a clear way. Because the implementation can be made in a lot of different ways. One possible implementation could be:

class EventRegistrationValidator
  def inititalize(event)
    @event = event
  end

  def self.errors_for(event)
    new(event).errors
  end

  def errors
    [name_is_blank, is_not_in_the_future].compact
  end

  private

  attr_reader :event

  def name_is_blank
    [:name, :blank] if event.name.blank?
  end

  def is_not_in_the_future
    return [:starts_at, :blank] if event.starts_at.blank?

    if event.starts_at < Time.current
      [:starts_at, :must_be_in_the_future]
    end
  end
end

I like this implementation because I think that is clear and descriptive, but maybe you can do it better!

Conclusion

If we compare this example with a more traditional “Rails way” implementation. Maybe the “Rails way” implementation will be more easy, but I am sure it will be more complex

One advantage of this validators is that are very disposable and can be easily switched. Also you can use a different validator for each different context.

Also if you use this validators you will reduce the responsibility of your models using smaller objects with one responsibility.

¡And your tests will run much faster!

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.