acts_as_state_machine with auto named_scope powers May 21st, 2008

Rails 2.1 (Soon to be released?) brings us integrated named_scope‘ing. I’ve already mentioned named_scope here and there, and you can find a complete writeup on Ryan Daigle’s Blog

While working on projects on Rails Edge, we used to have the same pattern over and over.

class Mail < ActiveRecord::Base
  include AASM

  aasm_state :created
  aasm_state :sending
  aasm_state :sent
  aasm_state :failed
  aasm_state :delayed

  named_scope :created, :conditions => {:status => 'created'}
  named_scope :sending, :conditions => {:status => 'sending'}
  named_scope :sent, :conditions => {:status => 'sent'}
  named_scope :failed, :conditions => {:status => 'failed'}
  named_scope :delayed, :conditions => {:status => 'delayed'}
end

For each state we had, we created a named_scope, because it is so great and easy to be able to say Mail.failed.each .... However, this isn’t really DRY, and we can see a clear pattern here. When you think about it, you really should have a scope for every state you define. So I forked from AASM on github and started hacking away.

The result:

class Mail < ActiveRecord::Base
  include AASM

  aasm_state :created
  aasm_state :sending
  aasm_state :sent
  aasm_state :failed
  aasm_state :delayed
end

...
>> Mail.failed
=> []
>> Mail.sent
=> [#<Mail id: 1, email: "jan@domain.com", content: "hallo", :aasm_state: "sent", created_at: "2008-05-21 15:39:38", updated_at: "2008-05-21 15:39:38">, #<Mail id: 2, email: "jan@domain.com", status: "sent", created_at: "2008-05-21 15:39:39", updated_at: "2008-05-21 15:39:39">]

You can find my named_scope enabled AASM on github. At the moment only the master branch has been patched with my functionality, I’ll do the no_aasm_prefix branch somewhere this evening / tomorrow. Let me know if you’re using it, and what could be better This has all been implemented in the AASM Core now.

For those interested in the code, this is the bit that does the magic.

module AASM::NamedScopeMethods
  def self.add_named_scope base
    # Don't add unless it's a class which understands `named_scope`
    return unless base.respond_to? :named_scope

    base.extend AASM::NamedScopeMethods::ClassMethods
    base.class_eval do
      class << self
        alias_method :aasm_state_without_named_scope, :aasm_state
        alias_method :aasm_state, :aasm_state_with_named_scope
      end
    end
  end

  module ClassMethods
    def aasm_state_with_named_scope name, options = {}
      aasm_state_without_named_scope name, options

      self.named_scope name, :conditions => {self.aasm_column => name.to_s} unless self.scopes.include?(name)
    end
  end
end

Gotcha

While testing the plugin we found out 1 gotcha to this: do not define a aasm_state :new, because this will override the constructor of the class, and you’ll lose all creating-functionality.

Update

This is now default AASM behaviour as of this commit

tags: l 5 comments »