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

state_machines

> 编程语言
Open source

Adds support for creating state machines for attributes on any Ruby class

870 stars0 likes1 views
WebsiteGitHub

About

Adds support for creating state machines for attributes on any Ruby class

State Machines

State Machines adds support for creating state machines for attributes on any Ruby class.

Please note that multiple integrations are available for Active Model, Active Record, Mongoid and more in the State Machines organisation. If you want to save state in your database, you need one of these additional integrations.

Installation

Add this line to your application's Gemfile:

ruby
gem 'state_machines'

And then execute:

bash
bundle

Or install it yourself as:

bash
gem install state_machines

Usage

Example

Below is an example of many of the features offered by this plugin, including:

  • Initial states
  • Namespaced states
  • Transition callbacks
  • Conditional transitions
  • Coordinated state management guards
  • Asynchronous state machines (async: true)
  • State-driven instance behavior
  • Customized state values
  • Parallel events
  • Path analysis

Class definition:

…

Note the comment made on the initialize method in the class. In order for state machine attributes to be properly initialized, super() must be called. See StateMachines:MacroMethods for more information about this.

Using the above class as an example, you can interact with the state machine like so:

…

ruby class TorpedoSystem state_machine :bay_doors, initial: :closed do event :open do transition closed: :open end

event :close do
  transition open: :closed
end

end

state_machine :torpedo_status, initial: :loaded do event :fire_torpedo do # Can only fire torpedo if bay doors are open transition loaded: :fired, if_state: { bay_doors: :open } end

event :reload do
  # Can only reload if bay doors are closed (for safety)
  transition fired: :loaded, unless_state: { bay_doors: :open }
end

end end

system = TorpedoSystem.new system.fire_torpedo # => false (bay doors are closed)

system.open_bay_doors! system.fire_torpedo # => true (bay doors are now open)


### Multiple State Guards

* `:if_all_states` - Transition only if ALL specified state machines are in their respective states.
* `:unless_all_states` - Transition only if NOT ALL specified state machines are in their respective states.
* `:if_any_state` - Transition only if ANY of the specified state machines are in their respective states.
* `:unless_any_state` - Transition only if NONE of the specified state machines are in their respective states.

```ruby
class StarshipBridge
  state_machine :shields, initial: :down
  state_machine :weapons, initial: :offline
  state_machine :warp_core, initial: :stable

  state_machine :alert_status, initial: :green do
    event :red_alert do
      # Red alert if ANY critical system needs attention
      transition green: :red, if_any_state: { warp_core: :critical, shields: :down }
    end

    event :battle_stations do
      # Battle stations only if ALL combat systems are ready
      transition green: :battle, if_all_states: { shields: :up, weapons: :armed }
    end
  end
end

Error Handling

State guards provide comprehensive error checking:

ruby
# Referencing a non-existent state machine
event :invalid, if_state: { nonexistent_machine: :some_state }
# => ArgumentError: State machine 'nonexistent_machine' is not defined for StarshipBridge

# Referencing a non-existent state
event :another_invalid, if_state: { shields: :nonexistent_state }
# => ArgumentError: State 'nonexistent_state' is not defined in state machine 'shields'

…

ruby
class AutonomousDrone  true

  # Bang methods for strict error handling
  drone.power_up_async!  # => Async::Task (raises on failure)

  # Generic async event firing
  drone.fire_event_async(:teleport)  # => Async::Task
end

# Outside Async context - raises error
drone.launch_async  # => RuntimeError: launch_async must be called within an Async context

Thread Safety

Async state machines use enterprise-grade thread safety with concurrent-ruby:

ruby
# Concurrent operations are automatically thread-safe
threads = []
10.times do
  threads = 2.25.0'
  gem 'concurrent-ruby', '>= 1.3.5'
end

Note: These gems are only installed on supported platforms. JRuby/TruffleRuby won't attempt installation.

ruby
# Human-friendly names can be accessed for states/events
Vehicle.human_state_name(:first_gear)               # => "first gear"
Vehicle.human_alarm_state_name(:active)             # => "active"

Vehicle.human_state_event_name(:shift_down)         # => "shift down"
Vehicle.human_alarm_state_event_name(:enable)       # => "enable"

# States / events can also be references by the string version of their name
Vehicle.human_state_name('first_gear')              # => "first gear"
Vehicle.human_state_event_name('shift_down')        # => "shift down"

# Available transition paths can be analyzed for an object
vehicle.state_paths                                       # => [[# [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
vehicle.state_paths.events                                # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]

# Possible states can be analyzed for a class
Vehicle.state_machine.states.to_a                   # [#, #, ...]
Vehicle.state_machines[:state].states.to_a          # [#, #, ...]

# Find all paths that start and end on certain states
vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
                                                          #       #,
                                                          #       #
                                                          #    ]]
# Skipping state_machine and writing to attributes directly
vehicle.state = "parked"
vehicle.state                   # => "parked"
vehicle.state_name              # => :parked

# *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):
# vehicle.state = :parked

Testing

State Machines provides an optional TestHelper module with assertion methods to make testing state machines easier and more expressive.

Note: TestHelper is not required by default - you must explicitly require it in your test files.

Setup

First, require the test helper module, then include it in your test class:

ruby
# For Minitest
require 'state_machines/test_helper'

class VehicleTest  #
vehicle.ignite              # => true
vehicle.state               # => "idling"

This is referred to as an explicit event transition. The same behavior can also be achieved implicitly by setting the state event attribute and invoking the action associated with the state machine. For example:

ruby
vehicle = Vehicle.create        # => #
vehicle.state_event = 'ignite'  # => 'ignite'
vehicle.save                    # => true
vehicle.state                   # => 'idling'
vehicle.state_event             # => nil

As you can see, the ignite event was automatically triggered when the save action was called. This is particularly useful if you want to allow users to drive the state transitions from a web API.

See each integration's API documentation for more information on the implicit approach.

Symbols vs. Strings

In all of the examples used throughout the documentation, you'll notice that states and events are almost always referenced as symbols. This isn't a requirement, but rather a suggested best practice.

You can very well define your state machine with Strings like so:

ruby
class Vehicle
  state_machine initial: 'parked' do
    event 'ignite' do
      transition 'parked' => 'idling'
    end

    # ...
  end
end

You could even use numbers as your state / event names. The important thing to keep in mind is that the type being used for referencing states / events in your machine definition must be consistent. If you're using Symbols, then all states / events must use Symbols. Otherwise you'll encounter the following error:

ruby
class Vehicle
  state_machine do
    event :ignite do
      transition parked: 'idling'
    end
  end
end

# => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent

There is an exception to this rule. The consistency is only required within the definition itself. However, when the machine's helper methods are called with input from external sources, such as a web form, state_machine will map that input to a String / Symbol. For example:

ruby
class Vehicle
  state_machine initial: :parked do
    event :ignite do
      transition parked: :idling
    end
  end
end

v = Vehicle.new     # => #
v.state?('parked')  # => true
v.state?(:parked)   # => true

Note that none of this actually has to do with the type of the value that gets stored. By default, all state values are assumed to be string -- regardless of whether the state names are symbols or strings. If you want to store states as symbols instead you'll have to be explicit about it:

ruby
class Vehicle
  state_machine initial: :parked do
    event :ignite do
      transition parked: :idling
    end

    states.each do |state|
      self.state(state.name, :value => state.name.to_sym)
    end
  end
end

v = Vehicle.new     # => #
v.state?('parked')  # => true
v.state?(:parked)   # => true

…

ruby
class Vehicle
  state_machine initial: :parked do
    before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
    after_transition to: :parked do |vehicle, transition|
      vehicle.seatbelt = 'off'
    end

    event :ignite do
      transition from: :parked, to: :idling
    end
  end
end

Transition context

Some flexibility is provided around the context in which transitions can be defined. In almost all examples throughout the documentation, transitions are defined within the context of an event. If you prefer to have state machines defined in the context of a state either out of preference or in order to easily migrate from a different library, you can do so as shown below:

ruby
class Vehicle
  state_machine initial: :parked do
    # ...

    state :parked do
      transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?

      def speed
        0
      end
    end

    state :first_gear do
      transition to: :second_gear, on: :shift_up

      def speed
        10
      end
    end

    state :idling, :first_gear do
      transition to: :parked, on: :park
    end
  end
end

In the above example, there's no need to specify the from state for each transition since it's inferred from the context.

You can also define transitions completely outside the context of a particular state / event. This may be useful in cases where you're building a state machine from a data store instead of part of the class definition. See the example below:

ruby
class Vehicle
  state_machine initial: :parked do
    # ...

    transition parked: :idling, :on => [:ignite, :shift_up]
    transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
    transition [:idling, :first_gear] => :parked, on: :park
    transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?
  end
end

…

ruby
class Vehicle
  state_machine :state, initial: :parked do
    event :park do
      transition [:idling, :first_gear] => :parked
    end

    # ...
  end
end

Draw state machines

State machines includes a default STDIORenderer for debugging state machines without external dependencies. This renderer can be used to visualize the state machine in the console.

To use the renderer, simply call the draw method on the state machine:

ruby
Vehicle.state_machine.draw # Outputs the state machine diagram to the console

You can customize the output by pa

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rubyrubystate-machine

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 推出的简洁高效系统语言