A statesmanlike state machine library for Ruby 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: :created, 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
belongs_to :order, inverse_of: :order_transitions
end
Order.first.state_machine.current_state
# => "created"
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:
config/initializers/statesman.rb
:
Statesman.configure do
storage_adapter(Statesman::Adapters::ActiveRecord)
transition_class(OrderTransition)
end
Generate the transition model:
$ rails g statesman: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
Statesman.configure do
storage_adapter(Statesman::Adapters::ActiveRecord)
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 adapter.
Statesman.configure do
transition_class(OrderTransition)
end
Configure the transition model. For now that means serializing metadata to JSON.
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. 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(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(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.
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
.
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::GuardFailedError
or Statesman::TransitionFailedError
on failure.
Transition to the passed state, returning true
on success. Swallows all exceptions and returns false on failure.