Grouping validations


I’m pretty satisfied with how Rails handles common tasks like routing, validations, internationalization. There is one thing though I’m not happy with. And it’s, of course, grouping validations. I’m talking about the simple situations where you have a model that needs to group validations per steps. Something that the awesome DataMapper folks call contextual validations. DataMapper provides the following great API:

class Article
  include DataMapper::Resource

  property :id, Serial
  property :title, String
  property :picture_url, String
  property :body, Text
  property :published, Boolean

  # validations

  validates_presence_of :title, :when => [ :draft, :publish ]
  validates_presence_of :picture_url, :when => [ :publish ]
  validates_presence_of :body, :when => [ :draft, :publish ]
  validates_length_of :body, :when => [ :publish ], :minimum => 1000
  validates_absence_of :published, :when => [ :draft ]
end

Even if it’s the first time you see this DataMapper API I’m pretty sure you’d like to have the opportunity to write something like the following:

article.valid?(:draft)

article.valid?(:publish)

Now, I’m not going to reproduce this feature using ActiveRecord, although it would be nice to have that. I just want to share with you a recipe for doing something similar to contextual validations in a nice and clean way.

To cook this recipe with need the following ingredients:

  • a state machine

    I’m going to use state_machine

  • with_options

    A very nice class macro, kindly offered by ActiveSupport.

  • A fancy example

    I promised myself I won’t use a blog in my examples. Unfortunately, I’m going to use something very similar. Sorry about that.

Let’s start with a trivial example: Suppose you have a client that works with articles. In order to publish an article, the following fancy requirements must be meet:

  • A draft article should have at least one page and a title.
  • A article can be queued for review only if it has at least three tags and a short description.
  • An article queued for publication can be actually published only it has a long description and at least two keywords for SEO fancy people.

Now that we’ve got the business rules we can say an article can be in the following statuses:

  • draft
  • ready_for_review
  • queued_for_publication
  • published

Using the neat state_machine gem, we can do something like the following:

class Article < ActiveRecord::Base
  has_many :pages

  state_machine :status, initial: :draft do
    event :ready do
      transition draft: :ready_for_review
    end
    event :queue do
      transition ready_for_review: :queued_for_publication
    end
    event :publish do
      transition queued_for_publication: :published
    end
  end
end

I really like the DSL. Basically, the event macro creates an instance method, executing the method will transit to the proper state if any. An example will surely explain how it works better than me:

irb(main):003:0> a = Article.create! title: 'New article'
=> #<Article id: 3, title: "New article", tags: nil, short_description: nil, long_description: nil, keywords: nil, created_at: "2012-04-13 14:38:16", updated_at: "2012-04-13 14:38:16", status: "draft">
irb(main):004:0> a.status
=> "draft"
irb(main):005:0> a.ready
=> true
irb(main):006:0> a.status
=> "ready_for_review"
irb(main):007:0> a.queue
=> true
irb(main):008:0> a.status
=> "queued_for_publication"
irb(main):009:0> a.publish
=> true
irb(main):010:0> a.status
=> "published"

Simple and effective. If you’re not familiar with state_machine I strongly recommend you to take a look at the README. You’ll find out a lot of interesting and useful features.

As we just saw, now we have an easily way to track the current status of an article for our workflow. We just have to add the validations. Before we start writing them, let’s take a quick look at the other ingredients of this recipe: with_options. This class macro gives us the opportunity of writing less verbose code. A lot of people would say that it will help you to write DRY code. Actually, DRY does not mean “don’t type a lot”. It means: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.. So, I don’t like very much when people makes their code just shorter and say “It’s DRY”.

Well, we were talking about with_options. In Rails, there are a lot of methods that take an Hash as the last argument, and when you have pass several methods the very same options you can use with_options to make the call shorter. It has the nice side-effect of logically grouping calls:

# before

class User
  validates :name, presence: true
  validates :surname, presence: true
  validates :password, presence: true, if: -> user { user.new_record? }
end

# after

class User
  with_options presence: true do |user|
    validates :name
    validates :surname
    validates :password, if: -> user { user.new_record? }
  end
end
```

OK, now we know how to use `with_options` and we have a nice state_machine. The
only thing we have to do now is writing the validations. We'll proceed by state,
starting with the draft:

```ruby
with_options if: -> article { article.status?(:draft) } do |article|
  article.validates :title, presence: true
  article.validates :pages, presence: true
end

That’s all. The article.status?(:draft) is a nice API kindly offered by state_machine. And now we can give it a try:

irb(main):001:0> a = Article.new
=> #<Article id: nil, title: nil, tags: nil, short_description: nil, long_description: nil, keywords: nil, created_at: nil, updated_at: nil, status: "draft">
irb(main):002:0> a.valid?
=> false
irb(main):004:0> a.errors.full_messages
=> ["Title can't be blank", "Pages can't be blank"]

It seems to work as expected. Let’s save it:

irb(main):006:0> a.title = 'Awesome article'
=> "Awesome article"
irb(main):007:0> a.pages.build content: 'great content'
=> #<Page id: nil, content: "great content", article_id: nil>
irb(main):008:0> a.save
=> true
irb(main):008:0> a.status
=> "draft"

Then we have to add validations for the second step. We have to ensure that an article can be queued for publication only if it has a short description and at least three tags.

The former is trivial because we can just use the presence validation. But validating tags requires a custom validator. Let’s assume tags are stored in one string and are comma-separated values, a reasonable approach could be the following:

with_options if: -> article { article.status?(:ready_for_review) } do |article|
article.validates :short_description, presence: true
article.validates :long_description, presence: true
article.validates :tags, presence: true
article.validate :at_least_three_tags, if: 'tags.present?'
end

private
def at_least_three_tags
self.errors[:tags] = "can't be less than three" if self.tags.split(',').length < 3
end

Let’s give it a try:

irb(main):001:0> article = Article.last
=> #<Article id: 5, title: "Awesome article", tags: nil, short_description: nil, long_description: nil, keywords: nil, created_at: "2012-04-14 15:20:28", updated_at: "2012-04-14 15:20:28", status: "draft">
irb(main):002:0> article.ready
=> false
irb(main):003:0> article.errors.full_messages
=> ["Short description can't be blank", "Tags can't be blank"]
irb(main):004:0> article.short_description = 'short desc'
=> "short desc"
irb(main):006:0> article.ready
=> false
irb(main):007:0> article.errors.full_messages
=> ["Tags can't be blank"]
irb(main):012:0> article.tags = 'foo, bar'
=> "foo, bar"
irb(main):013:0> article.ready
=> false
irb(main):014:0> article.errors.full_messages
=> ["Tags can't be less than three"]
irb(main):015:0> article.tags = 'foo, bar, baz'
=> "foo, bar, baz"
irb(main):016:0> article.ready
=> true
irb(main):017:0> article.status
=> "ready_for_review"
irb(main):018:0>

It works as expected.

Now the last step, We can queue an article if it has a long description and at least two keywords. Quite similar to the previous step:

with_options if: -> article { article.status?(:queued_for_publication) } do |article|
  article.validates :long_description, presence: true
  article.validates :keywords, presence: true
  article.validate :at_least_two_keywords, if: 'keywords.present?'
end

def at_least_two_keywords
  self.errors[:keywords] = "can't be less than two" if self.keywords.split(',').length < 2
end

Now just some quick tests in the console:

irb(main):011:0> article = Article.last
=> #<Article id: 5, title: "Awesome article", tags: "foo, bar, baz", short_description: "short desc", long_description: nil, keywords: nil, created_at: "2012-04-14 15:20:28", updated_at: "2012-04-14 17:11:00", status: "ready_for_review">
irb(main):012:0> article.queue
=> false
irb(main):013:0> article.errors.full_messages
=> ["Long description can't be blank", "Keywords can't be blank"]
irb(main):014:0> article.long_description = 'very long and boring description'
=> "very long and boring description"
irb(main):015:0> article.queue
=> false
irb(main):016:0> article.errors.full_messages
=> ["Keywords can't be blank"]
irb(main):017:0> article.keywords = 'foo'
=> "foo"
irb(main):018:0> article.queue
=> false
irb(main):019:0> article.errors.full_messages
=> ["Keywords can't be less than two"]
irb(main):020:0> article.keywords = 'foo, baz'
=> "foo, baz"
irb(main):021:0> article.queue
=> true
irb(main):022:0> article.status
=> "queued_for_publication"
irb(main):023:0>

It seems to work as we wanted. We grouped validations in a nice and readable way. Let’s see how the article model looks like now:

class Article < ActiveRecord::Base
  has_many :pages

  state_machine :status, initial: :draft do
    event :ready do
      transition draft: :ready_for_review
    end
    event :queue do
      transition ready_for_review: :queued_for_publication
    end
    event :publish do
      transition queued_for_publication: :published
    end
  end

  with_options if: -> article { article.status?(:draft) } do |article|
    article.validates :title, presence: true
    article.validates :pages, presence: true
  end

  with_options if: -> article { article.status?(:ready_for_review) } do |article|
    article.validates :short_description, presence: true
    article.validates :tags, presence: true
    article.validate :at_least_three_tags, if: 'tags.present?'
  end

  with_options if: -> article { article.status?(:queued_for_publication) } do |article|
    article.validates :long_description, presence: true
    article.validates :keywords, presence: true
    article.validate :at_least_two_keywords, if: 'keywords.present?'
  end

  private
    def at_least_three_tags
    self.errors[:tags] = "can't be less than three" if self.tags.split(',').length < 3
  end

  def at_least_two_keywords
   self.errors[:keywords] = "can't be less than two" if self.keywords.split(',').length < 2
  end
end

This technique can be a good starting point if you want to build a wizard. You can create next and previous events to handle the transitions.