finite_machine
                                
                                
                                
                                    finite_machine copied to clipboard
                            
                            
                            
                        A minimal finite state machine with a straightforward syntax.
FiniteMachine
A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and transitions and register callbacks to watch for triggered transitions.
Features
- plain object state machine
 - easy custom object integration
 - natural DSL for declaring events, callbacks and exception handlers
 - callbacks for state and event changes
 - ability to check reachable state(s)
 - ability to check for terminal state(s)
 - transition guard conditions
 - dynamic choice pseudostates
 - thread safe
 
Installation
Add this line to your application's Gemfile:
gem "finite_machine"
Then execute:
$ bundle
Or install it yourself as:
$ gem install finite_machine
Contents
- 1. Usage
 - 2. API
- 2.1 new
 - 2.2 define
 - 2.3 current
 - 2.4 initial
 - 2.5 terminal
 - 2.6 is?
 - 2.7 trigger
- 2.7.1 :auto_methods
 
 - 2.8 can? and cannot?
 - 2.9 target
- 2.9.1 :alias_target
 
 - 2.10 restore!
 - 2.11 states
 - 2.12 events
 
 - 3. States and Transitions
- 3.1 Triggering transitions
 - 3.2 Dangerous transitions
 - 3.3 Multiple from states
 - 3.4 any_state transitions
 - 3.5 Collapsing transitions
 - 3.6 Silent transitions
 - 3.7 Logging transitions
 - 3.8 Conditional transitions
- 3.8.1 Using a Proc
 - 3.8.2 Using a Symbol
 - 3.8.3 Using a String
 - 3.8.4 Combining transition conditions
 
 - 3.9 Choice pseudostates
- 3.9.1 Dynamic choice conditions
 - 3.9.2 Multiple from states
 
 
 - 4. Callbacks
- 4.1 on_(enter|transition|exit)
 - 4.2 on_(before|after)
 - 4.3 once_on
 - 4.4 Execution sequence
 - 4.5 Callback parameters
 - 4.6 Duplicate callbacks
 - 4.7 Fluid callbacks
 - 4.8 Methods inside callbacks
 - 4.9 Cancelling callbacks
 - 4.10 Asynchronous callbacks
 - 4.11 Instance callbacks
 
 - 5. Error Handling
- 5.1 Using target
 
 - 6. Stand-alone
- 6.1 Creating a Definition
 - 6.2 Targeting definition
 - 6.3 Definition inheritance
 
 - 7. Integration
- 7.1 Plain Ruby Objects
 - 7.2 ActiveRecord
 - 7.3 Transactions
 
 - 8. Tips
 
1. Usage
Here is a very simple example of a state machine:
fm = FiniteMachine.new do
  initial :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
  on_before(:ready) { |event| ... }
  on_exit(:yellow)  { |event| ... }
  on_enter(:green)  { |event| ... }
  on_after(:stop)   { |event| ... }
end
By calling the new method on FiniteMachine, you gain access to a powerful DSL for expressing transitions and registering callbacks.
Having declared the states and transitions, you can check current state:
fm.current # => :red
And then trigger transitions using the trigger:
fm.trigger(:ready)
Or you can use direct method calls:
fm.ready
Read States and Transitions and Callbacks sections for more details.
Alternatively, you can construct the state machine like a regular object using the same DSL methods. Similar machine could be reimplemented as follows:
fm = FiniteMachine.new(initial: :red)
fm.event(:ready, :red    => :yellow)
fm.event(:go,    :yellow => :green)
fm.event(:stop,  :green  => :red)
fm.on_before(:ready) { |event| ... }
fm.on_exit(:yellow)  { |event| ... }
fm.on_enter(:green)  { |event| ... }
fm.on_after(:stop)   { |event| ... }
2. API
2.1 new
In most cases you will want to create an instance of FiniteMachine class using the new method. At the bare minimum you need specify the transition events inside a block using the event helper:
fm = FiniteMachine.new do
  initial :green
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
end
Alternatively, you can skip block definition and instead call DSL methods directly on the state machine instance:
fm = FiniteMachine.new
fm.initial(:green)
fm.event(:slow, :green  => :yellow)
fm.event(:stop, :yellow => :red)
fm.event(:ready,:red    => :yellow)
fm.event(:go,   :yellow => :green)
As a guiding rule, any method exposed via DSL is available as a regular method call on the state machine instance.
2.2 define
To create a reusable definition for a state machine use define method. By calling define you're creating an anonymous class that can act as a factory for state machines. For example, below we create a TrafficLights class that contains our state machine definition:
TrafficLights = FiniteMachine.define do
  initial :green
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
end
Then we can create however many instance of above class:
lights_fm_a = TrafficLights.new
lights_fm_b = TrafficLights.new
Each instance will start in consistent state:
lights_fm_a.current # => :green
lights_fm_b.current # => :green
We can then trigger event for one instance and not the other:
lights_fm_a.slow
lights_fm_a.current # => :yellow
lights_fm_b.current # => :green
2.3 current
The FiniteMachine allows you to query the current state by calling the current method.
fm.current  # => :red
2.4 initial
There are number of ways to provide the initial state in FiniteMachine depending on your requirements.
By default the FiniteMachine will be in the :none state and you will need to provide an explicit event to transition out of this state.
fm = FiniteMachine.new do
  event :init,  :none   => :green
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
fm.current # => :none
fm.init    # => true
fm.current # => :green
If you specify initial state using the initial helper, then the state machine will be created already in that state and an implicit init event will be created for you and automatically triggered upon the state machine initialization.
fm = FiniteMachine.new do
  initial :green   # fires init event that transitions from :none to :green state
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
fm.current # => :green
Or by passing named argument :initial like so:
fm = FiniteMachine.new(initial: :green) do
  ...
end
If you want to defer setting the initial state, pass the :defer option to the initial helper. By default FiniteMachine will create init event that will allow to transition from :none state to the new state.
fm = FiniteMachine.new do
  initial :green, defer: true # Defer calling :init event
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
fm.current # => :none
fm.init    # execute initial transition
fm.current # => :green
If your target object already has init method or one of the events names redefines init, you can use different name by passing :event option to initial helper.
fm = FiniteMachine.new do
  initial :green, event: :start, defer: true # Rename event from :init to :start
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
fm.current # => :none
fm.start   # => call the renamed event
fm.current # => :green
By default the initial does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the silent option set to false like so:
fm = FiniteMachine.new do
  initial :green, silent: false  # callbacks are triggered
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
2.5 terminal
To specify a final state FiniteMachine uses the terminal method.
fm = FiniteMachine.new do
  initial :green
  terminal :red
  event :slow, :green  => :yellow
  event :stop, :yellow => :red
  event :go,   :red    => :green
end
When the terminal state has been specified, you can use terminated? method on the state machine instance to verify if the terminal state has been reached or not.
fm.terminated?  # => false
fm.slow         # => true
fm.terminated?  # => false
fm.stop         # => true
fm.terminated?  # => true
The terminal can accept more than one state.
fm = FiniteMachine.new do
  initial :open
  terminal :close, :canceled
  event :resolve, :open => :close
  event :decline, :open => :canceled
end
And the terminal state can be checked using terminated?:
fm.decline
fm.terminated? # => true
2.6 is?
To verify whether or not a state machine is in a given state, FiniteMachine uses is? method. It returns true if the machine is found to be in the given state, or false otherwise.
fm.is?(:red)    # => true
fm.is?(:yellow) # => false
Moreover, you can use helper methods to check for current state using the state name itself like so
fm.red?     # => true
fm.yellow?  # => false
2.7 trigger
Transition events can be fired by calling the trigger method with the event name and remaining arguments as data. The return value is either true or false depending whether the transition succeeded or not:
fm.trigger(:ready) # => true
fm.trigger(:ready, "one", "two", "three") # => true
By default, the FiniteMachine automatically converts all the transition event names into methods:
fm.ready # => true
fm.ready("one", "two", "three") # => true
Please see States and Transitions for in-depth treatment of firing transitions.
2.7.1 :auto_methods
By default, all event names will be converted by FiniteMachine into method names. This also means that you won't be able to use event names such as :fail or :trigger as these are already defined on the machine instance. In situations when you wish to use any event name for your event names use :auto_methods keyword to disable automatic methods generation. For example, to define :fail event:
fm = FiniteMachine.new(auto_methods: false) do
  initial :green
  event :fail, :green => :red
end
And then you can use trigger to fire the event:
fm.trigger(:fail)
fm.current # => :red
2.8 can? and cannot?
To verify whether or not an event can be fired, FiniteMachine provides can? or cannot? methods. can? checks if FiniteMachine can fire a given event, returning true, otherwise, it will return false. The cannot? is simply the inverse of can?.
fm.can?(:ready)    # => true
fm.can?(:go)       # => false
fm.cannot?(:ready) # => false
fm.cannot?(:go)    # => true
The can? and cannot? helper methods take into account the :if and :unless conditions applied to events. The set of values that :if or :unless condition takes as block parameter can be passed in directly via can? and cannot? methods' arguments, after the name of the event. For instance,
fm = FiniteMachine.new do
  initial :green
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red, if: ->(_, param) { :breaks == param }
end
fm.can?(:slow) # => true
fm.can?(:stop) # => false
fm.slow                    # => true
fm.can?(:stop, :breaks)    # => true
fm.can?(:stop, :no_breaks) # => false
2.9 target
If you need to execute some external code in the context of the current state machine, pass that object as a first argument to new method.
Assuming we have a simple Engine class that holds an internal state whether the car's engine is on or off:
class Engine
  def initialize
    @engine = false
  end
  def turn_on
    @engine = true
  end
  def turn_off
    @engine = false
  end
  def engine_on?
    @engine
  end
end
And given an instance of Engine class:
engine = Engine.new
You can provide a context to a state machine by passing it as a first argument to a new call. You can then reference this context inside the callbacks by calling the target helper:
fm = FiniteMachine.new(engine) do
  initial :neutral
  event :start, :neutral => :one, unless: "engine_on?"
  event :stop,  :one => :neutral
  on_before_start { |event| target.turn_on }
  on_after_stop { |event| target.turn_off }
end
For more complex example see Integration section.
2.9.1 :alias_target
If you wish to better express the intention behind the context object, in particular when calling actions in callbacks, you can use the :alias_target option:
engine = Engine.new
fm = FiniteMachine.new(engine, alias_target: :engine) do
  initial :neutral
  event :start, :neutral => :one, unless: "engine_on?"
  event :stop, :none => :neutral, if: "engine_on?"
  on_before_start { |event| engine.turn_on }
  on_after_stop { |event| engine.turn_off }
end
Alternatively, you can use the alias_target helper method:
engine = Engine.new
Car = FiniteMachine.define do
  alias_target :engine
  initial :neutral
  event :start, :neutral => :one, if: "engine_on?"
  event :stop, :none => :neutral, if: "engine_on?"
  on_before_start { |event| engine.turn_on }
  on_after_stop { |event| engine.turn_off }
end
Then to link Car definition with Engine instance, pass the Engine instance as a first argument:
car = Car.new(engine)
Triggering start event will change Engine instance state from false to true:
engine.engine_on? # => false
car.start
car.current       # => :one
engine.engine_on? # => true
2.10 restore!
In order to set the machine to a given state and thus skip triggering callbacks use the restore! method:
fm.restore!(:neutral)
This method may be suitable when used testing your state machine or in restoring the state from datastore.
2.11 states
You can use the states method to return an array of all the states for a given state machine.
fm.states # => [:none, :green, :yellow, :red]
2.12 events
To find out all the event names supported by the state machine issue events method:
fm.events # => [:init, :ready, :go, :stop]
3. States and Transitions
The FiniteMachine DSL exposes the event helper to define possible state transitions.
The event helper accepts as a first argument the transition's name which will later be used to create
method on the FiniteMachine instance. As a second argument the event accepts an arbitrary number of states either
in the form of :from and :to hash keys or by using the state names themselves as key value pairs.
event :start, from: :neutral, to: :first
# or
event :start, :neutral => :first
Once specified, the FiniteMachine will create custom methods for transitioning between each state. The following methods trigger transitions for the example state machine.
- ready
 - go
 - stop
 
You can always opt out from automatic method generation by using :auto_methods option.
3.1 Triggering transitions
In order to transition to the next reachable state, simply call the event's name on the FiniteMachine instance. If the transition succeeds the true value is returned, otherwise false.
fm.ready         # => true
fm.current       # => :yellow
If you prefer you can also use trigger method to fire any event by its name:
fm.trigger(:ready)  # => true
Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback as well as used by any present guarding conditions.
fm.go("Piotr!")  # => true
fm.current       # => :green
By default FiniteMachine will swallow all exceptions when and return false on failure. If you prefer to be notified when illegal transition occurs see Dangerous transitions.
3.2 Dangerous transitions
When you declare event, for instance ready, the FiniteMachine will provide a dangerous version with a bang ready!. In the case when you attempt to perform illegal transition or FiniteMachine throws internal error, the state machine will propagate the errors. You can use handlers to decide how to handle errors on case by case basis see 6. Error Handling
fm.ready!  #  => raises FiniteMachine::InvalidStateError
If you prefer you can also use trigger! method to fire event:
fm.trigger!(:ready)
3.3 Multiple from states
If an event transitions from multiple states to the same state then all the states can be grouped into an array. Alternatively, you can create separate events under the same name for each transition that needs combining.
fm = FiniteMachine.new do
  initial :neutral
  event :start,  :neutral             => :one
  event :shift,  :one                 => :two
  event :shift,  :two                 => :three
  event :shift,  :three               => :four
  event :slow,   [:one, :two, :three] => :one
end
3.4 any_state transitions
The FiniteMachine offers few ways to transition out of any state. This is particularly useful when the machine already defines many states.
You can use any_state as the name for a given state, for instance:
event :run, from: any_state, to: :green
# or
event :run, any_state => :green
Alternatively, you can skip the any_state call and just specify to state:
event :run, to: :green
All the above run event definitions will always transition the state machine into :green state.
3.5 Collapsing transitions
Another way to specify state transitions under single event name is to group all your state transitions into a single hash like so:
fm = FiniteMachine.new do
  initial :initial
  event :bump, :initial => :low,
               :low     => :medium,
               :medium  => :high
end
The same can be more naturally rewritten also as:
fm = FiniteMachine.new do
  initial :initial
  event :bump, :initial => :low
  event :bump, :low     => :medium
  event :bump, :medium  => :high
end
3.6 Silent transitions
The FiniteMachine allows to selectively silence events and thus prevent any callbacks from firing. Using the silent option passed to event definition like so:
fm = FiniteMachine.new do
  initial :yellow
  event :go    :yellow => :green, silent: true
  event :stop, :green => :red
end
fm.go   # no callbacks
fm.stop # callbacks are fired
3.7 Logging transitions
To help debug your state machine, FiniteMachine provides :log_transitions option.
FiniteMachine.new(log_transitions: true) do
  ...
end
3.8 Conditional transitions
Each event takes an optional :if and :unless options which act as a predicate for the transition. The :if and :unless can take a symbol, a string, a Proc or an array. Use :if option when you want to specify when the transition should happen. If you want to specify when the transition should not happen then use :unless option.
3.8.1 Using a Proc
You can associate the :if and :unless options with a Proc object that will get called right before transition happens. Proc object gives you ability to write inline condition instead of separate method.
fm = FiniteMachine.new do
  initial :green
  event :slow, :green => :yellow, if: -> { return false }
end
fm.slow    # doesn't transition to :yellow state
fm.current # => :green
Condition by default receives the current context, which is the current state machine instance, followed by extra arguments.
fm = FiniteMachine.new do
  initial :red
  event :go, :red => :green,
        if: ->(context, a) { context.current == a }
end
fm.go(:yellow) # doesn't transition
fm.go          # raises ArgumentError
Note If you specify condition with a given number of arguments then you need to call an event with the exact number of arguments, otherwise you will get ArgumentError. Thus in above scenario to prevent errors specify condition like so:
if: ->(context, *args) { ... }
Provided your FiniteMachine is associated with another object through target helper. Then the target object together with event arguments will be passed to the :if or :unless condition scope.
class Engine
  def initialize
    @engine = false
  end
  def turn_on
    @engine = true
  end
  def turn_off
    @engine = false
  end
  def engine_on?
    @engine
  end
end
engine = Engine.new
engine.turn_on
car = FiniteMachine.new(engine) do
  initial :neutral
  event :start, :neutral => :one, if: ->(target, state) do
    state ? target.engine_on : target.engine_off
  end
end
fm.start(false)
fm.current        # => :neutral
engine.engine_on? # => false
fm.start(true)
fm.current        # => :one
engine.engine_on? # => true
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See 4.9 Cancelling callbacks
3.8.2 Using a Symbol
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
fm = FiniteMachine.new(engine) do
  initial :neutral
  event :start, :neutral => :one, if: :engine_on?
end
3.8.3 Using a String
Finally, it's possible to use string that will be evaluated using eval and needs to contain valid Ruby code. It should only be used when the string represents a short condition.
fm = FiniteMachine.new(engine) do
  initial :neutral
  event :start, :neutral => :one, if: "engine_on?"
end
3.8.4 Combining transition conditions
When multiple conditions define whether or not a transition should happen, an Array can be used. Furthermore, you can apply both :if and :unless to the same transition.
fm = FiniteMachine.new do
  initial :green
  event :slow, :green => :yellow,
    if: [ -> { return true }, -> { return true} ],
    unless: -> { return true }
  event :stop, :yellow => :red
end
The transition only runs when all the :if conditions and none of the unless conditions are evaluated to true.
3.9 Choice pseudostates
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to select only one outgoing transition.
You can implement the conditional branch as ordinary events grouped under the same name and use familiar :if/:unless conditions:
fm = FiniteMachine.define do
  initial :green
  event :next, :green => :yellow, if: -> { false }
  event :next, :green => :red,    if: -> { true }
end
fm.current # => :green
fm.next
fm.current # => :red
The same conditional logic can be implemented using much shorter and more descriptive style using choice method:
fm = FiniteMachine.new do
  initial :green
  event :next, from: :green do
    choice :yellow, if: -> { false }
    choice :red,    if: -> { true }
  end
end
fm.current # => :green
fm.next
fm.current # => :red
3.9.1 Dynamic choice conditions
Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
fm = FiniteMachine.new do
  initial :green
  event :next, from: :green do
    choice :yellow, if: ->(context, a) { a < 1 }
    choice :red,    if: ->(context, a) { a > 1 }
    default :red
  end
end
fm.current # => :green
fm.next(0)
fm.current # => :yellow
If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the default state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify default choice.
3.9.2 Multiple from states
Similarly to event definitions, you can specify the event to transition from a group of states:
FiniteMachine.new do
  initial :red
  event :next, from: [:yellow, :red] do
    choice :pink, if: -> { false }
    choice :green
  end
end
Or from any state using the :any state name like so:
FiniteMachine.new do
  initial :red
  event :next, from: :any do
    choice :pink, if: -> { false }
    choice :green
  end
end
4. Callbacks
You can register a callback to listen for state transitions and events triggered, and based on these perform custom actions. There are five callbacks available in FiniteMachine:
on_before- triggered before any transitionon_exit- triggered when leaving any stateon_transition- triggered during any transitionon_enter- triggered when entering any stateon_after- triggered after any transition
Use the state or event name as a first parameter to the callback helper followed by block with event argument and a list arguments that you expect to receive like so:
on_enter(:green) { |event, a, b, c| ... }
When you subscribe to the :green state change, the callback will be called whenever someone triggers event that transitions in or out of that state. The same will happen on subscription to event ready, namely, the callback will be called each time the state transition method is triggered regardless of the states it transitions from or to.
fm = FiniteMachine.new do
  initial :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
  on_before :ready do |event, time1, time2, time3|
    puts "#{time1} #{time2} #{time3} Go!" }
  end
  on_before :go do |event, name|
    puts "Going fast #{name}"
  end
  on_before(:stop) { |event| ... }
end
fm.ready(1, 2, 3)
fm.go("Piotr!")
Note Regardless of how the state is entered or exited, all the associated callbacks will be executed. This provides means for guaranteed initialization and cleanup.
4.1 on_(enter|transition|exit)
The on_enter callback is executed before given state change is fired. By passing state name you can narrow down the listener to only watch out for enter state changes. Otherwise, all enter state changes will be watched.
The on_transition callback is executed when given state change happens. By passing state name you can narrow down the listener to only watch out for transition state changes. Otherwise, all transition state changes will be watched.
The on_exit callback is executed after a given state change happens. By passing state name you can narrow down the listener to only watch out for exit state changes. Otherwise, all exit state changes will be watched.
4.2 on_(before|after)
The on_before callback is executed before a given event happens. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
This callback is executed after a given event happened. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
4.3 once_on
FiniteMachine allows you to listen on initial state change or when the event is fired first time by using the following 5 types of callbacks:
once_on_enteronce_on_transitiononce_on_exitonce_beforeonce_after
4.4 Execution sequence
Assuming we have the following event specified:
event :go, :red => :yellow
Then by calling go event the following callbacks sequence will be executed:
on_before- generic callback beforeanyeventon_before :go- callback before thegoeventon_exit- generic callback for exit fromanystateon_exit :red- callback for the:redstate exiton_transition- callback for transition fromanystate toanystateon_transition :yellow- callback for the:redto:yellowtransitionon_enter- generic callback for entry toanystateon_enter :yellow- callback for the:yellowstate entryon_after- generic callback afteranyeventon_after :go- callback after thegoevent
4.5 Callback parameters
All callbacks as a first argument yielded to a block receive the TransitionEvent object with the following attributes:
name- the event name`from- the state transitioning from`to- the state transitioning to`
followed by the rest of arguments that were passed to the event method.
fm = FiniteMachine.new do
  initial :red
  event :ready, :red => :yellow
  on_before_ready do |event, time|
    puts "lights switching from #{event.from} to #{event.to} in #{time} seconds"
  end
end
fm.ready(3)
#  => "lights switching from red to yellow in 3 seconds"
4.6 Duplicate callbacks
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
Given the following state machine instance:
fm = FiniteMachine.new do
  initial :green
  event :slow, :green => :yellow
  on_enter(:yellow) { puts "this is run first" }
  on_enter(:yellow) { puts "then this is run" }
end
Triggerring the :slow event results in:
fm.slow
# => "this is run first"
# => "then this is run"
4.7 Fluid callbacks
Callbacks can also be specified as full method calls separated with underscores:
fm = FiniteMachine.define do
  initial :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
  on_before_ready { |event| ... }
  on_before_go    { |event| ... }
  on_before_stop  { |event| ... }
end
4.8 Methods inside callbacks
Given a class Car:
class Car
  attr_accessor :reverse_lights
  def turn_reverse_lights_off
    @reverse_lights = false
  end
  def turn_reverse_lights_on
    @reverse_lights = true
  end
end
We can easily manipulate state for an instance of a Car class:
car = Car.new
By defining finite machine using the instance:
fm = FiniteMachine.new(car) do
  initial :neutral
  event :forward, [:reverse, :neutral] => :one
  event :back,    [:neutral, :one] => :reverse
  on_enter_reverse { |event| target.turn_reverse_lights_on }
  on_exit_reverse  { |event| target.turn_reverse_lights_off }
end
Note that you can also fire events from callbacks.
fm = FiniteMachine.new do
  initial :neutral
  event :forward, [:reverse, :neutral] => :one
  event :back,    [:neutral, :one] => :reverse
  on_enter_reverse { |event| forward("Piotr!") }
  on_exit_reverse  { |event, name| puts "Go #{name}" }
end
Then triggerring :back event gives:
fm.back  # => Go Piotr!
For more complex example see Integration section.
4.9 Cancelling callbacks
A simple way to prevent transitions is to use 3 Conditional transitions.
There are times when you want to cancel transition in a callback. For example, you have logic which allows transition to happen only under certain complex conditions. Using cancel_event inside the on_(enter|transition|exit) or on_(before|after) callbacks will stop all the callbacks from firing and prevent current transition from happening.
For example, the following state machine cancels any event leaving :red state:
fm = FiniteMachine.new do
  initial :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
  on_exit :red do |event|
    ...
    cancel_event
  end
end
Then firing :ready event will not transition out of the current :red state:
fm.current  # => :red
fm.ready
fm.current  # => :red
4.10 Asynchronous callbacks
By default all callbacks are run synchronously. In order to add a callback that runs asynchronously, you need to pass second :async argument like so:
on_enter(:green, :async) do |event| ... end
# or
on_enter_green(:async) { |event| }
This will ensure that when the callback is fired it will run in separate thread outside of the main execution thread.
4.11 Instance callbacks
When defining callbacks you are not limited to the FiniteMachine block definition. After creating an instance, you can register callbacks the same way as before by calling on and supplying the type of notification and state/event you are interested in.
For example, given the following state machine:
fm = FiniteMachine.new do
  initial :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
end
We can add callbacks as follows:
fm.on_enter(:yellow) { |event| ... }
# or
fm.en_enter_yellow { |event| ... }
5. Error Handling
By default, the FiniteMachine will throw an exception whenever the machine is in invalid state or fails to transition.
FiniteMachine::TransitionErrorFiniteMachine::InvalidStateErrorFiniteMachine::InvalidCallbackError
You can attach specific error handler using the 'handle' with the name of the error as a first argument and a callback to be executed when the error happens. The handle receives a list of exception class or exception class names, and an option :with with a name of the method or a Proc object to be called to handle the error. As an alternative, you can pass a block.
fm = FiniteMachine.new do
  initial :green, event: :start
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
  handle FiniteMachine::InvalidStateError do |exception|
    # run some custom logging
    raise exception
  end
  handle FiniteMachine::TransitionError, with: -> { |exception| ... }
end
5.1 Using target
You can pass an external context as a first argument to the FiniteMachine initialization that will be available as context in the handler block or :with value. For example, the log_error method is made available when :with option key is used:
class Logger
  def log_error(exception)
    puts "Exception : #{exception.message}"
  end
end
fm = FiniteMachine.new(logger) do
  initial :green
  event :slow, :green  => :yellow
  event :stop, :yellow => :red
  handle "InvalidStateError", with: :log_error
end
6. Stand-alone
FiniteMachine allows you to separate your state machine from the target class so that you can keep your concerns broken in small maintainable pieces.
6.1 Creating a Definition
You can turn a class into a FiniteMachine by simply subclassing FiniteMachine::Definition. As a rule of thumb, every single public method of the FiniteMachine is available inside your class:
class Engine < FiniteMachine::Definition
  initial :neutral
  event :forward, [:reverse, :neutral] => :one
  event :shift, :one => :two
  event :back,  [:neutral, :one] => :reverse
  on_enter :reverse do |event|
    target.turn_reverse_lights_on
  end
  on_exit :reverse do |event|
    target.turn_reverse_lights_off
  end
  handle FiniteMachine::InvalidStateError do |exception|
    ...
  end
end
6.2 Targeting definition
The next step is to instantiate your state machine and use a custom class instance to load specific context.
For example, having the following Car class:
class Car
  def turn_reverse_lights_off
    @reverse_lights = false
  end
  def turn_reverse_lights_on
    @reverse_lights = true
  end
  def reverse_lights?
    @reverse_lights ||= false
  end
end
Thus, to associate Engine to Car do:
car = Car.new
engine = Engine.new(car)
car.reverse_lignts?  # => false
engine.back
car.reverse_lights?  # => true
Alternatively, create method inside the Car that will do the integration like so:
class Car
  ... #  as above
  def engine
    @engine ||= Engine.new(self)
  end
end
6.3 Definition inheritance
You can create more specialised versions of a generic definition by using inheritance. Assuming a generic state machine definition:
class GenericStateMachine < FiniteMachine::Definition
  initial :red
  event :start, :red => :green
  on_enter { |event| ... }
end
You can easily create a more specific definition that adds new events and more specific callbacks to the mix.
class SpecificStateMachine < GenericStateMachine
  event :stop, :green => :yellow
  on_enter(:yellow) { |event| ... }
end
Finally to use the specific state machine definition do:
specific_fsm = SpecificStateMachine.new
7. Integration
Since FiniteMachine is an object in its own right, it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into existing classes.
7.1 Plain Ruby Objects
In order to use FiniteMachine with an object, you need to define a method that will construct the state machine. You can implement the state machine using the new DSL or create a separate object that can be instantiated. To complete integration you will need to specify target context to allow state machine to communicate with the other methods inside the class like so:
class Car
  def turn_reverse_lights_off
    @reverse_lights = false
  end
  def turn_reverse_lights_on
    @reverse_lights = true
  end
  def reverse_lights_on?
    @reverse_lights || false
  end
  def gears
    @gears ||= FiniteMachine.new(self) do
      initial :neutral
      event :start, :neutral => :one
      event :shift, :one => :two
      event :shift, :two => :one
      event :back,  [:neutral, :one] => :reverse
      on_enter :reverse do |event|
        target.turn_reverse_lights_on
      end
      on_exit :reverse do |event|
        target.turn_reverse_lights_off
      end
      on_transition do |event|
        puts "shifted from #{event.from} to #{event.to}"
      end
    end
  end
end
Having written the class, you can use it as follows:
car = Car.new
car.gears.current      # => :neutral
car.reverse_lights_on? # => false
car.gears.start        # => "shifted from neutral to one"
car.gears.back         # => "shifted from one to reverse"
car.gears.current      # => :reverse
car.reverse_lights_on? # => true
7.2 ActiveRecord
In order to integrate FiniteMachine with ActiveRecord simply add a method with state machine definition. You can also define the state machine in separate module to aid reusability. Once the state machine is defined use the target helper to reference the current class. Having defined target you call ActiveRecord methods inside the callbacks to persist the state.
You can use the restore! method to specify which state the FiniteMachine should be put back into as follows:
class Account < ActiveRecord::Base
  validates :state, presence: true
  before_validation :set_initial_state, on: :create
  def set_initial_state
    self.state = manage.current
  end
  after_find :restore_state
  after_initialize :restore_state
  def restore_state
    manage.restore!(state.to_sym) if state.present?
  end
  def manage
    @manage ||= FiniteMachine.new(self) do
      initial :unapproved
      event :enqueue, :unapproved => :pending
      event :authorize, :pending => :access
      on_enter do |event|
        target.state = event.to
      end
    end
  end
end
account = Account.new
account.state   # => :unapproved
account.manage.enqueue
account.state   # => :pending
account.manage.authorize
account.state   # => :access
Please note that you do not need to call target.save inside callback, it is enough to just set the state. It is much more preferable to let the ActiveRecord object to persist when it makes sense for the application and thus keep the state machine focused on managing the state transitions.
7.3 Transactions
When using FiniteMachine with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 7.2 one can run event in transaction in the following way:
ActiveRecord::Base.transaction do
  account.manage.enqueue
end
If the transition fails it will raise TransitionError which will cause the transaction to rollback.
Please check the ORM of your choice if it supports database transactions.
8 Tips
Creating a standalone FiniteMachine brings a number of benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself. Ideally, you would test the machine in isolation and then integrate it with other objects or ORMs.
Contributing
- Fork it
 - Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
 
Code of Conduct
Everyone interacting in the FiniteMachine project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Copyright
Copyright (c) 2014 Piotr Murach. See LICENSE for further details.
