A minimal finite state machine with a straightforward syntax.
A minimal finite state machine with a straightforward syntax.
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.
Add this line to your application's Gemfile:
gem "finite_machine"Then execute:
$ bundleOr install it yourself as:
$ gem install finite_machineHere 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| ... }
endBy 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 # => :redAnd then trigger transitions using the trigger:
fm.trigger(:ready)Or you can use direct method calls:
fm.readyRead 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| ... }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
endAlternatively, 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.
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
endThen we can create however many instance of above class:
lights_fm_a = TrafficLights.new
lights_fm_b = TrafficLights.newEach instance will start in consistent state:
lights_fm_a.current # => :green
lights_fm_b.current # => :greenWe can then trigger event for one instance and not the other:
lights_fm_a.slow
lights_fm_a.current # => :yellow
lights_fm_b.current # => :greenThe FiniteMachine allows you to query the current state by calling the current method.
fm.current # => :redThere 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 # => :greenIf 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 # => :greenOr by passing named argument :initial like so:
fm = FiniteMachine.new(initial: :green) do
...
endIf 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 # => :greenIf 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 # => :greenBy 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
endTo 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
endWhen 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? # => trueThe terminal can accept more than one state.
fm = FiniteMachine.new do
initial :open
terminal :close, :canceled
event :resolve, :open => :close
event :decline, :open => :canceled
endAnd the terminal state can be checked using terminated?:
fm.decline
fm.terminated? # => trueTo 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) # => falseMoreover, you can use helper methods to check for current state using the state name itself like so
fm.red? # => true
fm.yellow? # => falseTransition 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") # => trueBy default, the FiniteMachine automatically converts all the transition event names into methods:
fm.ready # => true
fm.ready("one", "two", "three") # => truePlease see States and Transitions for in-depth treatment of firing transitions.
:auto_methodsBy 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. F
No open issues yet, or sync has not completed.