A statesmanlike state machine library for Ruby 1.9.3 and 2.0.
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.
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
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:
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:
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
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.
Machine.state(:some_state, initial: true) Machine.state(:another_state)
Define a new state and optionally mark as the initial state.
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(from: :some_state, to: another_state) do |object| object.some_boolean? end
Define a guard.
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(from: :some_state, to: another_state) do |object| object.side_effect end
Define a callback to run before a transition.
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(from: :some_state, to: another_state) do |object, transition| object.side_effect end
Define a callback to run after a successful transition.
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.
my_machine = Machine.new(my_model, transition_class: MyTransitionModel)
Initialize a new state machine instance.
my_model is required. If using the
my_model should have a
has_many association with
Returns the current state based on existing transition objects.
Returns a sorted array of all transition objects.
Returns the most recent transition object.
Returns true if the current state can transition to the passed state and all applicable guards pass.
Transition to the passed state, returning
true on success. Raises
Statesman::TransitionFailedError on failure.
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.