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 explicit support for adding your own code to customise transitions, nor is there any support for callbacks or event guards (although you can still do similar things fairly trivially). 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?

  • The three libraries mentioned above make available, as part of their DSL, a means of customising events/transitions with your own code. SimpleState makes no such provision, however you can mimic the behaviour quite easily as documented in example 3, below.
  • Similarly, some other libraries provide the ability to add guard conditions -- a condition which must be satisfied before a transition can take place. SimpleState also does explicitly support this, however it is possible by adapting example 3.
  • 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.

If SimpleState's limitations are too much for you, then you are probably better off choosing one of the other libraries instead.

Examples

Example 1: Basic usage

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...

Example 2: Events in multiple states

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

Example 3: Customising event transitions

If the built in event methods aren't sufficient and you need to do extra stuff to your class during a particular event, you can simply override the method; the original method is available via super:

class OverriddenEvent
  extend SimpleState

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

    state :started
  end

  def start!
    puts "Before super() : state=#{self.state}"
    ret = super
    puts "After super() : state=#{self.state}"
    ret
  end
end

OverriddenEvent.new.start!
# => Before super() : state=start
# => After super() : state=finished
# => :started

If the event transition isn't valid, super will simply return false, otherwise it will return the symbol representing the new state.

def start!
  if new_state = super
    puts "Started! The new state is #{self.state}"
  else
    puts "Could not start!"
  end

  new_state
end

machine = OverriddenEvent.new
machine.start!
=> Started! The new state is finished
=> :started

machine.start!
=> Could not start!
=> false

If you need to know whether a transition will be permitted before you call super(), SimpleState provides #event_permitted?, expecting you to provide a symbol representing the event.

machine.event_permitted?(:start)
# => true|false

This also provides an easy means for creating guard conditions:

def start!
  if event_permitted?(:start) && SomeExternalService.can_start?(self)
    super
  end
end

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.