ActiveHash and SuperEnum!

  • December 26, 2010
  • Tim Harrison

Jeff Dean’s ActiveHash is a great gem for status-type associations. In this post I show how I extend ActiveHash to improve readability.

A NewsItem model might belong to Status to indicate values like Published, Draft, and Deleted. ActiveHash lets you do ActiveRecord type associations, but without having to hit the database.

class Status < ActiveHash::Base
  include ActiveHash::Enum
  self.data = [
    {:id => 1, :name => 'Draft'},
    {:id => 2, :name => 'Published'},
    {:id => 3, :name => 'Deleted'}
  ]
  enum_accessor :name
end

# == Schema Information
#
# Table name: news_items
#
#  id         :integer(4)      not null, primary key
#  title      :string(255)
#  body       :text
#  status_id  :integer(4)
#  created_at :datetime
#  updated_at :datetime
#
class NewsItem < ActiveRecord::Base
  belongs_to :status
  scope :not_deleted, 
        :conditions => [ 'news_items.status_id != ?', 
                         Status::DELETED.id ]
end

NewsItem is a standard ActiveRecord model, but notice that Status inherits from ActiveHash::Base. Status will not touch the database, but you can use it like a regular ActiveRecord model. E.g., in your controllers and models:

news_item = NewsItem.create(:status => Status::DRAFT)
if news_item.status == Status::PUBLISHED
  # ...
end

And in your formtastics, you can treat Status like you would an ActiveRecord model:

  f.input :status, :collection => Status.all

In the above examples I’m using the ActiveHash::Enum module to magically add the DRAFT, PUBLISHED, and DELETED enumerations. Super handy. However, I have to leave my mark on everything, so I extended ActiveHash a little. Instead of Status::PUBLISHED, I like to write Status.published. And I’d also like to be able to easily test the value of a Status instance with shorthand such as @status.published? and @status.deleted?:

  @news_item = NewsItem.create(:status => NewsItem.draft)
  # ...
  if @news_item.status.draft?
    # ...
  elsif @news_item.status.published?
    # ...
  elsif @news_item.status.deleted?
    # ...
  end

To do the above I create a little helper module in RAILS_ROOT/lib/active_hash/super_enum:

module ActiveHash
  module SuperEnum
    def self.included(base)
      base.send(:include, Enum)
      base.extend(Methods)
    end

    module Methods
      def insert(record)
        super
        loud = constant_for(record.attributes[@enum_accessor])
        return nil if loud.blank?
        quiet = loud.downcase
        self.class_eval <<-RUBY
        def #{quiet}?
          self.id == #{loud}.id
        end
        class << self
          def #{quiet}
            #{loud}
          end
        end
        RUBY
      end
    end
  end
end

Now in my Status module I do this:

require 'active_hash/super_enum'
class Status < ActiveHash::Base
  include ActiveHash::SuperEnum
  self.data = [
    {:id => 1, :name => 'Draft'},
    {:id => 2, :name => 'Published'},
    {:id => 3, :name => 'Deleted'}
  ]
  enum_accessor :name
end

Thus in your controllers and models:

news_item = NewsItem.create(:status => Status.draft)
if news_item.status.published?
  # ...
end

Functionally, SuperEnum is the same as ActiveHash::Enum. SuperEnum just shouts a little less for those of us with sensitive eyes. In any case, you should all be using ActiveHash. It saves you from writing useless database migrations, db initialization tasks, and it probably saves trees.

What are other people using for Status-type associations?


Comments