SimplestStatus — An Enhanced Enum Implementation for Any Version of Rails

Ryan Stenberg, Former Developer

Article Category: #Code

Posted on

While working on a recent project, I ended up with three different models with integer-type status columns and nearly identical status-related functionality. They looked something like this:

class Post < ActiveRecord::Base
  DRAFT     = 0
  PREVIEW   = 1
  PUBLISHED = 2
  ARCHIVED  = 3

  STATUSES = {
    DRAFT     => :draft,
    PREVIEW   => :preview,
    PUBLISHED => :published,
    ARCHIVED  => :archived
  }

  STATUSES.each do |value, name|
    scope name, -> { where(status: value) }

    define_method "#{name}?" do
      status == value
    end

    define_method name do
      update_attributes(status: value)
    end
  end

  validates :status, presence: true, inclusion: { in: STATUSES.keys }
end

Having an integer-type status column gave me more flexibility over the number of statuses — as well as the flexibility to change the name of a status without having to make any database migrations (which would have been the case if I was storing strings). In my case, this was worth the tradeoff of the database having a different understanding of the data than the application.

After the third model, it was time to DRY things up. First, I thought about the simplest DSL to get me all the status functionality I needed, which led to the following:

class Post < ActiveRecord::Base
  statuses :draft,
           :preview,
           :published,
           :archived
end

As a result, SimplestStatus was born!

Enter Enum

After implementing an extendable module (SimplestStatus) with the statuses DSL, I ran the idea past Mike, who mentioned the whole idea seemed just like enum. At the time, I hadn't heard of enum before.

If you're in that same boat, enum is a model-level method introduced in Rails 4.1 that wraps integer-type fields and provides a convenient set of functionality:

class Post < ActiveRecord::Base
  enum status: [ :draft, :preview, :published, :archived ]

  # or

  enum status: {
    draft:     0,
    preview:   1,
    published: 2,
    archived:  3
  }
end

This will get you class-level scopes as well as instance level setters and predicate methods — all things I had implemented myself in SimplestStatus. So why go with SimplestStatus?

Why SimplestStatus?

Compatibility

SimplestStatus's biggest advantage over enum is that it doesn't depend on a certain version of Rails. It works with practically every version (only tested as far back as 2.0.5, but may well work fine with earlier versions than that).

Reader Methods and Constants

If a field is backed by an integer column, I think there's value in exposing the integer to the application. Say you wanted to implement a Post#next_status method that cycles through the list of statuses:

class Post < ActiveRecord::Base
  extend SimplestStatus

  statuses :draft, :preview, :published, :archived

  def next_status
    Post.statuses[(status + 1) % Post.statuses.size]
  end
end

Looking at enum, I wasn't a huge fan of the way it overwrote the reader method:

post.status # => 'draft'

SimplestStatus generates constants matching each status's name:

Post::DRAFT     # => 0
Post::PREVIEW   # => 1
Post::PUBLISHED # => 2
Post::ARCHIVED  # => 3

Rather than having to remember the underlying integers, constants provide an expressive way to look up values. I found these helpful when setting up factories:

FactoryGirl.define do
  factory :post do
    # ...
    status Post::PUBLISHED
    # ...
  end

  trait :archived do
    status Post::ARCHIVED
  end
end

Label Methods

Let's say we added a status :crowd_favorite to our list on the Post model.

With enum, you call post.status and get a string matching the original name you gave it:

post.status # => 'crowd_favorite'

For use in a view or form, you'd probably end up capitalizing/titleizing everywhere.

SimplestStatus provides a #status_label method:

post.status_label # => 'Crowd Favorite'

It also provides a label-friendly list of statuses for use in a form select:

Post.statuses.for_select
# => [['Draft', 0], ['Preview', 1], ['Published', 2], ['Archived', 3], ['Crowd Favorite', 4]]

Validations

SimplestStatus will automatically add the following validations:

validates :status, presence: true, inclusion: { in: proc { statuses.values } }

..and More!

Check out the README to see all the other things you can do with SimplestStatus.

What's Next?

The original intent was to provide the simplest DSL possible to get all the convenient status functionality you could want. If there are things you find yourself doing around these integer-backed fields that you'd like to see in SimplestStatus, open a Github issue or leave a comment below. I'd love to hear feedback.

When you're working on a Rails app having models with a status — regardless of the Rails version — consider SimplestStatus!

Related Articles