state_machine
https://github.com/pluginaweek/state_machine
Ruby
Adds support for creating state machines for attributes on any Ruby class
StateMachine::MacroMethods#state_machine
Creates a new state machine with the given name. The default name, if not
specified, is <tt>:state</tt>.
Configuration options:
* <tt>:attribute</tt> - The name of the attribute to store the state value
in. By default, this is the same as the name of the machine.
* <tt>:initial</tt> - The initial state of the attribute. This can be a
static state or a lambda block which will be evaluated at runtime
(e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
Default is nil.
* <tt>:initialize</tt> - Whether to automatically initialize the attribute
by hooking into #initialize on the owner class. Default is true.
* <tt>:action</tt> - The instance method to invoke when an object
transitions. Default is nil unless otherwise specified by the
configured integration.
* <tt>:namespace</tt> - The name to use for namespacing all generated
state / event instance methods (e.g. "heater" would generate
:turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
Default is nil.
* <tt>:integration</tt> - The name of the integration to use for adding
library-specific behavior to the machine. Built-in integrations
include :active_model, :active_record, :data_mapper, :mongo_mapper, and
:sequel. By default, this is determined automatically.
Configuration options relevant to ORM integrations:
* <tt>:plural</tt> - The pluralized version of the name. By default, this
will attempt to call +pluralize+ on the name. If this method is not
available, an "s" is appended. This is used for generating scopes.
* <tt>:messages</tt> - The error messages to use when invalidating
objects due to failed transitions. Messages include:
* <tt>:invalid</tt>
* <tt>:invalid_event</tt>
* <tt>:invalid_transition</tt>
* <tt>:use_transactions</tt> - Whether transactions should be used when
firing events. Default is true unless otherwise specified by the
configured integration.
This also expects a block which will be used to actually configure the
states, events and transitions for the state machine. *Note* that this
block will be executed within the context of the state machine. As a
result, you will not be able to access any class methods unless you refer
to them directly (i.e. specifying the class name).
For examples on the types of state machine configurations and blocks, see
the section below.
== Examples
With the default name/attribute and no configuration:
class Vehicle
state_machine do
event :park do
...
end
end
end
The above example will define a state machine named "state" that will
store the value in the +state+ attribute. Every vehicle will start
without an initial state.
With a custom name / attribute:
class Vehicle
state_machine :status, :attribute => :status_value do
...
end
end
With a static initial state:
class Vehicle
state_machine :status, :initial => :parked do
...
end
end
With a dynamic initial state:
class Vehicle
state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
...
end
end
== Class Methods
The following class methods will be automatically generated by the
state machine based on the *name* of the machine. Any existing methods
will not be overwritten.
* <tt>human_state_name(state)</tt> - Gets the humanized value for the
given state. This may be generated by internationalization libraries if
supported by the integration.
* <tt>human_state_event_name(event)</tt> - Gets the humanized value for
the given event. This may be generated by internationalization
libraries if supported by the integration.
For example,
class Vehicle
state_machine :state, :initial => :parked do
event :ignite do
transition :parked => :idling
end
event :shift_up do
transition :idling => :first_gear
end
end
end
Vehicle.human_state_name(:parked) # => "parked"
Vehicle.human_state_name(:first_gear) # => "first gear"
Vehicle.human_state_event_name(:park) # => "park"
Vehicle.human_state_event_name(:shift_up) # => "shift up"
== Instance Methods
The following instance methods will be automatically generated by the
state machine based on the *name* of the machine. Any existing methods
will not be overwritten.
* <tt>state</tt> - Gets the current value for the attribute
* <tt>state=(value)</tt> - Sets the current value for the attribute
* <tt>state?(name)</tt> - Checks the given state name against the current
state. If the name is not a known state, then an ArgumentError is raised.
* <tt>state_name</tt> - Gets the name of the state for the current value
* <tt>human_state_name</tt> - Gets the human-readable name of the state
for the current value
* <tt>state_events(requirements = {})</tt> - Gets the list of events that
can be fired on the current object's state (uses the *unqualified* event
names)
* <tt>state_transitions(requirements = {})</tt> - Gets the list of
transitions that can be made on the current object's state
* <tt>state_paths(requirements = {})</tt> - Gets the list of sequences of
transitions that can be run from the current object's state
* <tt>fire_state_event(name, *args)</tt> - Fires an arbitrary event with
the given argument list. This is essentially the same as calling the
actual event method itself.
The <tt>state_events</tt>, <tt>state_transitions</tt>, and <tt>state_paths</tt>
helpers all take an optional set of requirements for determining what's
available for the current object. These requirements include:
* <tt>:from</tt> - One or more states to transition from. If none are
specified, then this will be the object's current state.
* <tt>:to</tt> - One or more states to transition to. If none are
specified, then this will match any to state.
* <tt>:on</tt> - One or more events to transition on. If none are
specified, then this will match any event.
* <tt>:guard</tt> - Whether to guard transitions with the if/unless
conditionals defined for each one. Default is true.
For example,
class Vehicle
state_machine :state, :initial => :parked do
event :ignite do
transition :parked => :idling
end
event :park do
transition :idling => :parked
end
end
end
vehicle = Vehicle.new
vehicle.state # => "parked"
vehicle.state_name # => :parked
vehicle.human_state_name # => "parked"
vehicle.state?(:parked) # => true
# Changing state
vehicle.state = 'idling'
vehicle.state # => "idling"
vehicle.state_name # => :idling
vehicle.state?(:parked) # => false
# Getting current event / transition availability
vehicle.state_events # => [:park]
vehicle.park # => true
vehicle.state_events # => [:ignite]
vehicle.state_events(:from => :idling) # => [:park]
vehicle.state_events(:to => :parked) # => []
vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
vehicle.ignite # => true
vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
vehicle.state_transitions(:on => :ignite) # => []
# Getting current path availability
vehicle.state_paths # => [
# [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
# #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
# ]
vehicle.state_paths(:guard => false) # =>
# [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
# #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
# ]
# Fire arbitrary events
vehicle.fire_state_event(:park) # => true
== Attribute initialization
For most classes, the initial values for state machine attributes are
automatically assigned when a new object is created. However, this
behavior will *not* work if the class defines an +initialize+ method
without properly calling +super+.
For example,
class Vehicle
state_machine :state, :initial => :parked do
...
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state # => "parked"
In the above example, no +initialize+ method is defined. As a result,
the default behavior of initializing the state machine attributes is used.
In the following example, a custom +initialize+ method is defined:
class Vehicle
state_machine :state, :initial => :parked do
...
end
def initialize
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
vehicle.state # => nil
Since the +initialize+ method is defined, the state machine attributes
never get initialized. In order to ensure that all initialization hooks
are called, the custom method *must* call +super+ without any arguments
like so:
class Vehicle
state_machine :state, :initial => :parked do
...
end
def initialize(attributes = {})
...
super()
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state # => "parked"
Because of the way the inclusion of modules works in Ruby, calling
<tt>super()</tt> will not only call the superclass's +initialize+, but
also +initialize+ on all included modules. This allows the original state
machine hook to get called properly.
If you want to avoid calling the superclass's constructor, but still want
to initialize the state machine attributes:
class Vehicle
state_machine :state, :initial => :parked do
...
end
def initialize(attributes = {})
...
initialize_state_machines
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state # => "parked"
You may also need to call the +initialize_state_machines+ helper manually
in cases where you want to change how static / dynamic initial states get
set. For example, the following example forces the initialization of
static states regardless of their current value:
class Vehicle
state_machine :state, :initial => :parked do
state nil, :idling
...
end
def initialize(attributes = {})
@state = 'idling'
initialize_state_machines(:static => :force) do
...
end
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state # => "parked"
The above example is also noteworthy because it demonstrates how to avoid
initialization issues when +nil+ is a valid state. Without passing in
<tt>:static => :force</tt>, state_machine would never have initialized
the state because +nil+ (the default attribute value) would have been
interpreted as a valid current state. As a result, state_machine would
have simply skipped initialization.
== States
All of the valid states for the machine are automatically tracked based
on the events, transitions, and callbacks defined for the machine. If
there are additional states that are never referenced, these should be
explicitly added using the StateMachine::Machine#state or
StateMachine::Machine#other_states helpers.
When a new state is defined, a predicate method for that state is
generated on the class. For example,
class Vehicle
state_machine :initial => :parked do
event :ignite do
transition all => :idling
end
end
end
...will generate the following instance methods (assuming they're not
already defined in the class):
* <tt>parked?</tt>
* <tt>idling?</tt>
Each predicate method will return true if it matches the object's
current state. Otherwise, it will return false.
== Attribute access
The actual value for a state is stored in the attribute configured for the
state machine. In most cases, this is the same as the name of the state
machine. For example:
class Vehicle
attr_accessor :state
state_machine :state, :initial => :parked do
...
state :parked, :value => 0
start :idling, :value => 1
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb712da60 @state=0>
vehicle.state # => 0
vehicle.parked? # => true
vehicle.state = 1
vehicle.idling? # => true
The most important thing to note from the example above is what it means
to read from and write to the state machine's attribute. In particular,
state_machine treats the attribute (+state+ in this case) like a basic
attr_accessor that's been defined on the class. There are no special
behaviors added, such as allowing the attribute to be written to based on
the name of a state in the machine. This is the case for a few reasons:
* Setting the attribute directly is an edge case that is meant to only be
used when you want to skip state_machine altogether. This means that
state_machine shouldn't have any effect on the attribute accessor
methods. If you want to change the state, you should be using one of
the events defined in the state machine.
* Many ORMs provide custom behavior for the attribute reader / writer - it
may even be defined by your own framework / method implementation just
the example above showed. In order to avoid having to worry about the
different ways an attribute can get written, state_machine just makes
sure that the configured value for a state is always used when writing
to the attribute.
If you were interested in accessing the name of a state (instead of its
actual value through the attribute), you could do the following:
vehicle.state_name # => :idling
== Events and Transitions
Events defined on the machine are the interface to transitioning states
for an object. Events can be fired either directly (through the method
generated for the event) or indirectly (through attributes defined on
the machine).
For example,
class Vehicle
include DataMapper::Resource
property :id, Serial
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
end
state_machine :alarm_state, :initial => :active do
event :disable do
transition all => :off
end
end
end
# Fire +ignite+ event directly
vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
vehicle.ignite # => true
vehicle.state # => "idling"
vehicle.alarm_state # => "active"
# Fire +disable+ event automatically
vehicle.alarm_state_event = 'disable'
vehicle.save # => true
vehicle.alarm_state # => "off"
In the above example, the +state+ attribute is transitioned using the
+ignite+ action that's generated from the state machine. On the other
hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
attribute that automatically gets fired when the machine's action (+save+)
is invoked.
For more information about how to configure an event and its associated
transitions, see StateMachine::Machine#event.
== Defining callbacks
Within the +state_machine+ block, you can also define callbacks for
transitions. For more information about defining these callbacks,
see StateMachine::Machine#before_transition, StateMachine::Machine#after_transition,
and StateMachine::Machine#around_transition, and StateMachine::Machine#after_failure.
== Namespaces
When a namespace is configured for a state machine, the name provided
will be used in generating the instance methods for interacting with
states/events in the machine. This is particularly useful when a class
has multiple state machines and it would be difficult to differentiate
between the various states / events.
For example,
class Vehicle
state_machine :heater_state, :initial => :off, :namespace => 'heater' do
event :turn_on do
transition all => :on
end
event :turn_off do
transition all => :off
end
end
state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
event :turn_on do
transition all => :active
end
event :turn_off do
transition all => :off
end
end
end
The above class defines two state machines: +heater_state+ and +alarm_state+.
For the +heater_state+ machine, the following methods are generated since
it's namespaced by "heater":
* <tt>can_turn_on_heater?</tt>
* <tt>turn_on_heater</tt>
* ...
* <tt>can_turn_off_heater?</tt>
* <tt>turn_off_heater</tt>
* ..
* <tt>heater_off?</tt>
* <tt>heater_on?</tt>
As shown, each method is unique to the state machine so that the states
and events don't conflict. The same goes for the +alarm_state+ machine:
* <tt>can_turn_on_alarm?</tt>
* <tt>turn_on_alarm</tt>
* ...
* <tt>can_turn_off_alarm?</tt>
* <tt>turn_off_alarm</tt>
* ..
* <tt>alarm_active?</tt>
* <tt>alarm_off?</tt>
== Scopes
For integrations that support it, a group of default scope filters will
be automatically created for assisting in finding objects that have the
attribute set to one of a given set of states.
For example,
Vehicle.with_state(:parked) # => All vehicles where the state is parked
Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
*Note* that if class methods already exist with those names (i.e.
:with_state, :with_states, :without_state, or :without_states), then a
scope will not be defined for that name.
See StateMachine::Machine for more information about using integrations
and the individual integration docs for information about the actual
scopes that are generated.Edit
git clone [email protected]:pluginaweek/state_machine.git
cd state_machine
open lib/state_machine/macro_methods.rb
Contribute
# Make a new branchgit checkout -b -your-name--update-docs-StateMachine--MacroMethods-state_machine-for-pr
# Commit to gitgit add lib/state_machine/macro_methods.rbgit commit -m "better docs for StateMachine::MacroMethods#state_machine"
# Open pull requestgem install hub # on a mac you can `brew install hub`
hub fork
git push <your name> -your-name--update-docs-StateMachine--MacroMethods-state_machine-for-pr
hub pull-request
# Celebrate!