Adds support for creating state machines for attributes on any Ruby class
Adds support for creating state machines for attributes on any Ruby class
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.
Add this line to your application's Gemfile:
gem 'state_machines'And then execute:
bundleOr install it yourself as:
gem install state_machinesBelow is an example of many of the features offered by this plugin, including:
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
endState guards provide comprehensive error checking:
# 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 contextAsync state machines use enterprise-grade thread safety with concurrent-ruby:
# Concurrent operations are automatically thread-safe
threads = []
10.times do
threads = 2.25.0'
gem 'concurrent-ruby', '>= 1.3.5'
endNote: These gems are only installed on supported platforms. JRuby/TruffleRuby won't attempt installation.
# 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 = :parkedState 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.
First, require the test helper module, then include it in your test class:
# 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:
vehicle = Vehicle.create # => #
vehicle.state_event = 'ignite' # => 'ignite'
vehicle.save # => true
vehicle.state # => 'idling'
vehicle.state_event # => nilAs 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.
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:
class Vehicle
state_machine initial: 'parked' do
event 'ignite' do
transition 'parked' => 'idling'
end
# ...
end
endYou 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:
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 consistentThere 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:
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) # => trueNote 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:
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
endSome 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:
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
endIn 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:
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
endState 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:
Vehicle.state_machine.draw # Outputs the state machine diagram to the consoleYou can customize the output by pa
No open issues yet, or sync has not completed.