Edge State Machine

Edge State Machine is a complete state machine solution. It offers support for ActiveRecord, Mongoid and MongoMapper for persistence.

Supported Features

  • Multiple state machines per class each of them acting independently
  • Find errors in state machine definitions as early as possible
  • Transition guards
  • Multiple actions executed on transitions
  • Multiple actions executed on entering and exiting a state
  • No other dependencies for non-persistent state machines
  • Minimal dependencies for persistent ones

Installation

If you're using Rails + ActiveRecord + Bundler

# in your Gemfile
gem "edge-state-machine", :require => ["edge-state-machine", "active_record/edge-state-machine"]

# in your AR models that will use the state machine
include ::EdgeStateMachine
include ActiveRecord::EdgeStateMachine

state_machine do
state :available # first one is initial state
state :out_of_stock
state :discontinue

event :discontinue do
  transitions :to => :discontinue, :from => [:available, :out_of_stock], :on_transition => :do_discontinue
end
event :out_of_stock do
  transitions :to => :out_of_stock, :from => [:available, :discontinue]
end
event :available do
  transitions :to => :available, :from => [:out_of_stock], :on_transition => :send_alerts
end
end

If you're using Rails + Mongoid + Bundler

# in your Gemfile
gem "edge-state-machine", :require => ["edge-state-machine", "mongoid/edge-state-machine"]

# in your AR models that will use the state machine
include ::EdgeStateMachine
include Mongoid::EdgeStateMachine

state_machine do
state :available # first one is initial state
state :out_of_stock
state :discontinue

event :discontinue do
  transitions :to => :discontinue, :from => [:available, :out_of_stock], :on_transition => :do_discontinue
end
event :out_of_stock do
  transitions :to => :out_of_stock, :from => [:available, :discontinue]
end
event :available do
  transitions :to => :available, :from => [:out_of_stock], :on_transition => :send_alerts
end
end

If you're using Rails + MongoMapper + Bundler

# in your Gemfile
gem "edge-state-machine", :require => ["edge-state-machine", "mongo_mapper/edge-state-machine"]

# in your models that will use the state machine
include ::EdgeStateMachine
include MongoMapper::EdgeStateMachine

state_machine do
state :available # first one is initial state
state :out_of_stock
state :discontinue

event :discontinue do
  transitions :to => :discontinue, :from => [:available, :out_of_stock], :on_transition => :do_discontinue
end
event :out_of_stock do
  transitions :to => :out_of_stock, :from => [:available, :discontinue]
end
event :available do
  transitions :to => :available, :from => [:out_of_stock], :on_transition => :send_alerts
end
end

State Machine Examples

Microwave State Machine

class Microwave
state_machine :microwave do  # name should be optional, if the name is not present, it should have a default name
                             # we give state machines names, so we can pun many machines inside a class
  initial_state :unplugged # initial state should be optional, if the initial state is not present, the initial state will be the first defined state

  state :unplugged

  state :plugged

  state :door_opened do
    enter :light_on            # enter should be executed on entering the state
    exit  :light_off           # exit method should be executed on exiting the state
  end

  state :door_closed

  state :started_in_grill_mode do
    enter lambda { |t| p "Entering hate" }   # should have support for Procs
    exit  :grill_off
  end

  state :started do
    enter :microwaves_on
    exit  :microwaves_off
  end

  event :plug_in do
    transition :from => :unplugged, :to => :plugged
  end

  event :open_door do
    transition :from => :plugged, :to => :door_opened
  end

  event :close_door do
    transition :from => :door_opened, :to => :door_closed,
               :on_transition => :put_food_in_the_microwave # we can put many actions in an Array for the on_transition parameter
  end

  event :start do
    transition :from => :door_closed, :to => [:started, :started_in_grill_mode],
               :on_transition => :start_spinning_the_food,
               :guard => :grill_button_pressed?    # the method grill_button_pressed? should choose the next state 
  end

  event :stop do
      transition :from => [:started, :started_in_grill_mode], :to => :door_closed
  end
end  
end 

Dice State Machine

class Dice

state_machine do
  state :one
  state :two
  state :three
  state :four
  state :five
  state :six

  event :roll do
    transition :from => [:one, :two, :three, :four, :five, :six],
               :to => [:one, :two, :three, :four, :five, :six],
               :guard => :roll_result,
               :on_transition => :display_dice_rolling_animation
  end
end

def roll_result
  # return one of the states
end

def display_dice_rolling_animation
  # draw the new position of the dice
end
end

class User
state_machine do
  state :pending # first one is initial state
  state :active
  state :blocked # the user in this state can't sign in

  event :activate do
    transition :from => [:pending], :to => :active, :on_transition => :do_activate
  end

  event :block do
    transition :from => [:pending, :active], :to => :blocked
  end
end    
end

Other Examples

For other (more complex) examples, please check the following links:

Notes

For classes with multiple state machines, the state names, machine names must be unique per class.

The same thing with the event names.

Credits

The gem is based on Rick Olson's code of ActiveModel::StateMachine, axed from ActiveModel in this commit.

And on Krzysiek Heród's gem, Transitions, which added Mongoid support.