SimpleState

Yet another state machine library for Ruby.

Why another state machine library?

There are several existing implementations of state machines in Ruby, notably pluginaweek/state_machine, rubyist/aasm and ryan-allen/workflow. However, all felt rather heavy and cumbersome, when all I really needed was a lightweight means for setting the state of a class instance, and transitioning from one state to another.

There is no support for adding your own code to customise transitions, nor is is there any support for callbacks or event guards. It's called SimpleState for a reason! The library adds some helper methods to your class, keeps track of the valid states, makes sure that a transition is permitted, and that's about it.

Why use SimpleState?

  • Lightweight.
  • method_missing isn't used. ;)
  • No dependencies.
  • No extensions to core classes.
  • Tested on Ruby 1.8.6 (p287), 1.8.7 (p72), and 1.9.1 (p0).
  • Uses an API similar to Workflow, which I find to be more logical than that in the acts_as_state_machine family.

Why use something else?

  • SimpleState has no support for customising transitions with your own code.
  • No support for callbacks.
  • No support for guard conditions.
  • SimpleState forces you to use an attribute called state - other libraries let you choose whatever name you want.
  • Uses a class variable to keep track of transitions - doesn't lend itself all that well to subclassing your state machines.

The three libraries mentioned above have support for callbacks and guard conditions: if you need these features then you'd be better off choosing one of those libraries instead of SimpleState.

Examples

require 'rubygems'
require 'simple_state'

class SimpleStateMachine
  extend SimpleState  # Adds state_machine method to this class.

  state_machine do
    state :not_started do
      event :start,  :transitions_to => :started
    end

    state :started do
      event :finish, :transitions_to => :finished
      event :cancel, :transitions_to => :cancelled
    end

    state :finished
    state :cancelled
  end
end

SimpleState makes one assumption: that the first call to state in the state_machine block is the default state; every instance of SimpleStateMachine will begin with the state :not_started.

Note: if you define #initialize in your class, you should ensure that you call super or the default state won't get set.

The above example declares four states: not_started, started, finished and cancelled. If your instance is in the not_started state it may transition to the started state by calling SimpleStateMachine#start!. Once started it can then transition to finished or cancelled using SimpleStateMachine#finish! and SimpleStateMachine#cancel!.

Along with the bang methods for changing an instance's state, there are predicate methods which will return true or false depending on the current state of the instance.

instance = SimpleStateMachine.new  # Initial state will be :not_started

instance.not_started? # => true
instance.started?     # => false
instance.finished?    # => false
instance.cancelled?   # => false

instance.start!

instance.not_started? # => false
instance.started?     # => true
instance.finished?    # => false
instance.cancelled?   # => false

# etc...

It is possible for the same event to be used in multiple states:

state :not_started do
  event :start,  :transitions_to => :started
  event :cancel, :transitions_to => :cancelled  # <--
end

state :started do
  event :finish, :transitions_to => :finished
  event :cancel, :transitions_to => :cancelled  # <--
end

... or for the event to do something different depending on the object's current state:

state :not_started do
  event :start,  :transitions_to => :started
  event :cancel, :transitions_to => :cancelled_before_start  # <--
end

state :started do
  event :finish, :transitions_to => :finished
  event :cancel, :transitions_to => :cancelled               # <--
end

state :finished
state :cancelled
state :cancelled_before_start

ORM Integration

SimpleState should play nicely with your ORM of choice. When an object's state is set, YourObject#state= is called with a symbol representing the state. Simply add a string/enum property called state to your DataMapper class, or a state field to your ActiveRecord database and things should be fine. I confess to having no familiarity with Sequel, but I don't foresee any difficulty there either.

License

SimpleState is released under the MIT License; see LICENSE for details.