StateJacket

Build Status Dependency Status Code Climate

An Intuitive State Transition System

State machines are awesome but can be pretty daunting as a system grows. Keeping states, transitions, & events straight can be tricky. StateJacket simplifies things by isolating the management of states & transitions. Events are left out, making it much easier to reason about what states exist and how they transition to other states.

The examples below are somewhat contrived, but should clearly illustrate the usage.

The Basics

Install

$ gem install state_jacket

Define states & transitions for a simple turnstyle.

Turnstyle

require "state_jacket"

states = StateJacket::Catalog.new
states.add :open => [:closed, :error]
states.add :closed => [:open, :error]
states.add :error
states.lock

states.inspect # => {:open=>[:closed, :error], :closed=>[:open, :error], :error=>nil}
states.transitioners # => [:open, :closed]
states.terminators # => [:error]

states.can_transition? :open => :closed # => true
states.can_transition? :closed => :open # => true
states.can_transition? :error => :open # => false
states.can_transition? :error => :closed # => false

Next Steps

Lets model something a bit more complex.

Define states & transitions for a phone call.

Phone Call

require "state_jacket"

states = StateJacket::Catalog.new
states.add :idle => [:dialing]
states.add :dialing => [:idle, :connecting]
states.add :connecting => [:idle, :busy, :connected]
states.add :busy => [:idle]
states.add :connected => [:idle]
states.lock

states.transitioners # => [:idle, :dialing, :connecting, :busy, :connected]
states.terminators # => []

states.can_transition? :idle => :dialing # => true
states.can_transition? :dialing => [:idle, :connecting] # => true
states.can_transition? :connecting => [:idle, :busy, :connected] # => true
states.can_transition? :busy => :idle # => true
states.can_transition? :connected => :idle # => true
states.can_transition? :idle => [:dialing, :connected] # => false

Deep Cuts

Lets add state awareness and behavior to another class. We'll reuse the turnstyle states from the example from above.

require "state_jacket"

class Turnstyle
  attr_reader :states, :current_state

  def initialize
    @states = StateJacket::Catalog.new
    @states.add :open => [:closed, :error]
    @states.add :closed => [:open, :error]
    @states.add :error
    @states.lock
    @current_state = :closed
  end

  def open
    if states.can_transition? current_state => :open
      @current_state = :open
    else
      raise "Can't transition from #{@current_state} to :open"
    end
  end

  def close
    if states.can_transition? current_state => :closed
      @current_state = :closed
    else
      raise "Can't transition from #{@current_state} to :closed"
    end
  end

  def break
    @current_state = :error
  end
end

# example usage
turnstyle = Turnstyle.new
turnstyle.current_state # => :closed
turnstyle.open
turnstyle.current_state # => :open
turnstyle.close
turnstyle.current_state # => :closed
turnstyle.close # => RuntimeError: Can't transition from closed to :closed
turnstyle.open
turnstyle.current_state # => :open
turnstyle.open # => RuntimeError: Can't transition from open to :open
turnstyle.break
turnstyle.open # => RuntimeError: Can't transition from error to :open
turnstyle.close # => RuntimeError: Can't transition from error to :closed

Running the Tests

gem install state_jacket
gem unpack state_jacket
cd state_jacket-VERSION
rake