Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
S

statesman

> 编程语言
Open source

A statesmanlike state machine library.

1.9K stars0 likes1 views
WebsiteGitHub

About

A statesmanlike state machine library.

A statesmanlike state machine library.

For our policy on compatibility with Ruby and Rails versions, see COMPATIBILITY.md. Statesman is an opinionated state machine library designed to provide a robust audit trail and data integrity. It decouples the state machine logic from the underlying model and allows for easy composition with one or more model classes.

As such, the design of statesman is a little different from other state machine libraries:

  • State behaviour is defined in a separate, "state machine" class, rather than added directly onto a model. State machines are then instantiated with the model to which they should apply.
  • State transitions are also modelled as a class, which can optionally be persisted to the database for a full audit history. This audit history can include JSON metadata set during a transition.
  • Database indices are used to offer database-level transaction duplication protection.

Installation

To get started, just add Statesman to your Gemfile, and then run bundle:

ruby
gem 'statesman', '~> 13.0.0'

Usage

First, create a state machine based on Statesman::Machine:

…

Then, link it to your model:

ruby
class Order < ActiveRecord::Base
  has_many :order_transitions, autosave: false

  include Statesman::Adapters::ActiveRecordQueries[
    transition_class: OrderTransition,
    initial_state: :pending
  ]

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

Next, you'll need to create a further model to represent state transitions:

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

  validates :to_state, inclusion: { in: OrderStateMachine.states }

  belongs_to :order, inverse_of: :order_transitions
end

Now, you can start working with your state machine:

…

If you'd like, you can also define a template for a generic state machine, then alter classes which extend it as required:

…

Persistence

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

config/initializers/statesman.rb:

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

Generate the transition model:

bash
rails g statesman:active_record_transition Order OrderTransition

Your transition class should include Statesman::Adapters::ActiveRecordTransition if you're using the ActiveRecord adapter.

If you're using the ActiveRecord adapter and decide not to include the default updated_at column in your transition table, you'll need to configure the updated_timestamp_column option on the transition class, setting it to another column name (e.g. :updated_on) or nil.

And add an association from the parent model:

app/models/order.rb:

ruby
class Order < ActiveRecord::Base
  has_many :transitions, class_name: "OrderTransition", autosave: false

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

  # Optionally delegate some methods

  delegate :can_transition_to?,
           :current_state, :history, :last_transition, :last_transition_to,
           :transition_to!, :transition_to, :in_state?, to: :state_machine
end

Using PostgreSQL JSON column

By default, Statesman uses serialize to store the metadata in JSON format. It is also possible to use the PostgreSQL JSON column if you are using Rails 4 or 5. To do that

  • Change metadata column type in the transition model migration to json or jsonb

    ruby
    # Before
    t.text :metadata, default: "{}"
    # After (Rails 4)
    t.json :metadata, default: "{}"
    # After (Rails 5)
    t.json :metadata, default: {}
  • Remove the include Statesman::Adapters::ActiveRecordTransition statement from your transition model, which would've instructed ActiveRecord to serialize the metadata.
  • The module that you just removed enables customizing the updatated timestamp column as described above. Having removed it, if you want to customise your transition class's "updated timestamp column", you should define a .updated_timestamp_column method on your class and return the name of the column as a symbol, or nil if you don't want to record an updated timestamp on transitions.

Configuration

storage_adapter

ruby
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 will fallback to memory unless you specify a transition_class when instantiating your state machine. This allows you to only persist transitions on certain state machines in your app.

initial_transition

ruby
def state_machine
  @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition,
                                                 association_name: :transitions,
                                                 initial_transition: true)
end

By default Statesman does not record a transition to the initial state of the state machine.

You can configure Statesman to record a transition to the initial state, this will allow you to:

  • Keep an accurate record of the initial state even if configuration changes
  • Keep a record of how long the state machine spent in the initial state
  • Utilise a transition hook for the transition to the initial state

Class methods

Machine.state

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

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

Machine.transition

ruby
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

ruby
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. The guard will pass when it evaluates to a truthy value and fail when it evaluates to a falsey value (nil or false).

Machine.before_transition

ruby
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

ruby
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.

If you specify after_commit: true, the callback will be executed once the transition has been committed to the database.

Machine.after_transition_failure

ruby
Machine.after_transition_failure(from: :some_state, to: :another_state) do |object, exception|
  Logger.info("transition to #{exception.to} failed for #{object.id}")
end

Define a callback to run if Statesman::TransitionFailedError is raised during the execution of transition callbacks. to and from parameters are optional, a nil parameter means run after all transitions. The model object, and exception are passed as arguments to the callback. This is executed outside of the transaction wrapping other callbacks. If using transition! the exception is re-raised after these callbacks are executed.

Machine.after_guard_failure

ruby
Machine.after_guard_failure(from: :some_state, to: :another_state) do |object, exception|
  Logger.info("guard failed during transition to #{exception.to} for #{object.id}")
end

Define a callback to run if Statesman::GuardFailedError is raised during the execution of guard callbacks. to and from parameters are optional, a nil parameter means run after all transitions. The model object, and exception are passed as arguments to the callback. This is executed outside of the transaction wrapping other callbacks. If using transition! the exception is re-raised after these callbacks are executed.

Machine.new

ruby
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.

Machine.retry_conflicts

ruby
Machine.retry_conflicts { instance.transition_to(:new_state) }

Automatically retry the given block if a TransitionConflictError is raised. If you know you want to retry a transition if it fails due to a race condition call it from within this block. Takes an (optional) argument for the maximum number of retry attempts (defaults to 1).

Machine.states

Returns an array of all possible state names as strings.

Machine.successors

Returns a hash of states and the states it is valid for them to transition to.

ruby
Machine.successors

{
  "pending" => ["checking_out", "cancelled"],
  "checking_out" => ["purchased", "cancelled"],
  "purchased" => ["shipped", "failed"],
  "shipped" => ["refunded"]
}

Class constants

Adding a state to a state machine will automatically create a constant for the value, for example:

ruby
class OrderStateMachine
  include Statesman::Machine

  state :pending, initial: true
  state :checking_out
  state :cancelled

  # Constants created as a side effect of adding state
  transition from: PENDING, to: [CHECKING_OUT, CANCELLED]
end

OrderStateMachine::PENDING #=> "pending"
OrderStateMachine::CHECKING_OUT # => "checking_out"

Instance methods

Machine#current_state

Returns the current state based on existing transition objects.

Takes an optional keyword argument to force a reload of data from the database. e.g current_state(force_reload: true)

Machine#in_state?(:state_1, :state_2, ...)

Returns true if the machine is in any of the given states.

Machine#history

Returns a sorted array of all transition objects.

Machine#last_transition

Returns the most recent transition object.

Machine#last_transition_to(:state)

Returns the most recent transition object to a given state.

Machine#allowed_transitions

Returns an array of states you can transition_to from current state.

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 Statesman exceptions and returns false on failure. (NB. if your guard or callback code throws an exception, it will not be caught.)

Errors

Initialization errors

These errors are raised when the Machine and/or Model is initialized. A simple spec like

ruby
expec

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Ruby

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言