Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
A statesmanlike state machine library.
Ruby
branch: master

This branch is 1 commit ahead, 221 commits behind gocardless:master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
lib v0.2.1
spec
.gitignore
.rubocop.yml
.travis.yml
CHANGELOG.md
Gemfile
Guardfile
LICENSE.txt
README.md
Rakefile
circle.yml
statesman.gemspec

README.md

Statesman

A statesmanlike state machine library for Ruby 1.9.3 and 2.0.

Gem Version Build Status Code Climate

Statesman is a little different from other state machine libraries which tack state behaviour directly onto a model. A statesman state machine is defined as a separate class which is instantiated with the model to which it should apply. State transitions are also modelled as a class which can optionally be persisted to the database for a full audit history, including JSON metadata which can be set during a transition.

This data model allows for interesting things like using a different state machine depending on the value of a model attribute.

TL;DR Usage

class OrderStateMachine
  include Statesman::Machine

  state :pending, initial: true
  state :checking_out
  state :purchased
  state :shipped
  state :cancelled
  state :failed
  state :refunded

  transition from: :pending,      to: [:checking_out, :cancelled]
  transition from: :checking_out, to: [:purchased, :cancelled]
  transition from: :purchased,    to: [:shipped, :failed]
  transition from: :shipped,      to: :refunded

  guard_transition(to: :checking_out) do |order|
    order.products_in_stock?
  end

  before_transition(from: :checking_out, to: :cancelled) do |order, transition|
    order.reallocate_stock
  end

  before_transition(to: :purchased) do |order, transition|
    PaymentService.new(order).submit
  end

  after_transition(to: :purchased) do |order, transition|
    MailerService.order_confirmation(order).deliver
  end
end

class Order < ActiveRecord::Base
  has_many :order_transitions

  def state_machine
    OrderStateMachine.new(self, transition_class: OrderTransition)
  end
end

class OrderTransition < ActiveRecord::Base
  include Statesman::Adapters::ActiveRecordTransition

  belongs_to :order, inverse_of: :order_transitions
end

Order.first.state_machine.current_state
# => "pending"

Order.first.state_machine.can_transition_to?(:cancelled)
# => true/false

Order.first.state_machine.transition_to(:cancelled, optional: :metadata)
# => true/false

Order.first.state_machine.transition_to!(:cancelled)
# => true/exception

Persistence

By default Statesman stores transition history in memory only. It can be persisted by configuring Statesman to use a different adapter. For example, ActiveRecord within Rails:

config/initializers/statesman.rb:

Statesman.configure do
  storage_adapter(Statesman::Adapters::ActiveRecord)
end

Generate the transition model:

$ rails g statesman:active_record_transition Order OrderTransition

And add an association from the parent model:

app/models/order.rb:

class Order < ActiveRecord::Base
  has_many :order_transitions

  # Initialize the state machine
  def state_machine
    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
  end

  # Optionally delegate some methods
  delegate :can_transition_to?, :transition_to!, :transition_to, :current_state,
           to: :state_machine
end

Configuration

storage_adapter

Statesman.configure do
  storage_adapter(Statesman::Adapters::ActiveRecord)
  # ...or
  storage_adapter(Statesman::Adapters::Mongoid)
end

Statesman defaults to storing transitions in memory. If you're using rails, you can instead configure it to persist transitions to the database by using the ActiveRecord or Mongoid adapter.

Class methods

Machine.state

Machine.state(:some_state, initial: true)
Machine.state(:another_state)

Define a new state and optionally mark as the initial state.

Machine.transition

Machine.transition(from: :some_state, to: :another_state)

Define a transition rule. Both method parameters are required, to can also be an array of states (.transition(from: :some_state, to: [:another_state, :some_other_state])).

Machine.guard_transition

Machine.guard_transition(from: :some_state, to: another_state) do |object|
  object.some_boolean?
end

Define a guard. to and from parameters are optional, a nil parameter means guard all transitions. The passed block should evaluate to a boolean and must be idempotent as it could be called many times.

Machine.before_transition

Machine.before_transition(from: :some_state, to: another_state) do |object|
  object.side_effect
end

Define a callback to run before a transition. to and from parameters are optional, a nil parameter means run before all transitions. This callback can have side-effects as it will only be run once immediately before the transition.

Machine.after_transition

Machine.after_transition(from: :some_state, to: another_state) do |object, transition|
  object.side_effect
end

Define a callback to run after a successful transition. to and from parameters are optional, a nil parameter means run after all transitions. The model object and transition object are passed as arguments to the callback. This callback can have side-effects as it will only be run once immediately after the transition.

Machine.new

my_machine = Machine.new(my_model, transition_class: MyTransitionModel)

Initialize a new state machine instance. my_model is required. If using the ActiveRecord adapter my_model should have a has_many association with MyTransitionModel.

Instance methods

Machine#current_state

Returns the current state based on existing transition objects.

Machine#history

Returns a sorted array of all transition objects.

Machine#last_transition

Returns the most recent transition object.

Machine#can_transition_to?(:state)

Returns true if the current state can transition to the passed state and all applicable guards pass.

Machine#transition_to!(:state)

Transition to the passed state, returning true on success. Raises Statesman::GuardFailedError or Statesman::TransitionFailedError on failure.

Machine#transition_to(:state)

Transition to the passed state, returning true on success. Swallows all exceptions and returns false on failure.

Testing with RSpec

require 'spec_helper'
describe OrderStateMachine do
  describe 'states' do
    before :each do
      @order = create(:order)
      @state_machine = OrderStateMachine.new(@order, transition_class: OrderTransition)
    end

    it 'has initial state pending' do
      expect(@state_machine.current_state).to eql "pending"
    end

    context "pending" do
      it "transitions to checking_out" do
        @state_machine.transition_to!(:checking_out)
        expect(@state_machine.current_state).to eql "checking_out"
      end
      it "transitions to cancelled" do
        @state_machine.transition_to!(:cancelled)
        expect(@state_machine.current_state).to eql "cancelled"
      end
    end
  end
end

GoCardless ♥ open source. If you do too, come join us.

Something went wrong with that request. Please try again.