Class: StateMachines::Machine

Overview

Represents a state machine for a particular attribute. State machines consist of states, events and a set of transitions that define how the state changes after a particular event is fired.

A state machine will not know all of the possible states for an object unless they are referenced somewhere in the state machine definition. As a result, any unused states should be defined with the other_states or state helper.

Actions

When an action is configured for a state machine, it is invoked when an object transitions via an event. The success of the event becomes dependent on the success of the action. If the action is successful, then the transitioned state remains persisted. However, if the action fails (by returning false), the transitioned state will be rolled back.

As shown, even though the state is set prior to calling the save action on the object, it will be rolled back to the original state if the action fails. Note that this will also be the case if an exception is raised while calling the action.

Indirect transitions

In addition to the action being run as the result of an event, the action can also be used to run events itself. For example, using the above as an example:

As can be seen, the save action automatically invokes the event stored in the state_event attribute (:ignite in this case).

One important note about using this technique for running transitions is that if the class in which the state machine is defined also defines the action being invoked (and not a superclass), then it must manually run the StateMachine hook that checks for event attributes.

For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel, the default action (save) is already defined in a base class. As a result, when a state machine is defined in a model / resource, StateMachine can automatically hook into the save action.

On the other hand, the Vehicle class from above defined its own save method (and there is no save method in its superclass). As a result, it must be modified like so:

This will add in the functionality for firing the event stored in the state_event attribute.

Callbacks

Callbacks are supported for hooking before and after every possible transition in the machine. Each callback is invoked in the order in which it was defined. See StateMachines::Machine#before_transition and StateMachines::Machine#after_transition for documentation on how to define new callbacks.

Note that callbacks only get executed within the context of an event. As a result, if a class has an initial state when it's created, any callbacks that would normally get executed when the object enters that state will not get triggered.

If you need callbacks to get triggered when an object is created, this should be done by one of the following techniques:

Use a before :create or equivalent hook:

class Vehicle
before :create, :track_initial_transition
state_machine do
...
end
end

Set an initial state and use the correct event to create the object with the proper state, resulting in callbacks being triggered and the object getting persisted (note that the :pending state is actually stored as nil):

Canceling callbacks

Callbacks can be canceled by throwing :halt at any point during the callback. For example,

...
throw :halt
...

If a before callback halts the chain, the associated transition and all later callbacks are canceled. If an after callback halts the chain, the later callbacks are canceled, but the transition is still successful.

These same rules apply to around callbacks with the exception that any around callback that doesn't yield will essentially result in :halt being thrown. Any code executed after the yield will behave in the same way as after callbacks.

Note that if a before callback fails and the bang version of an event was invoked, an exception will be raised instead of returning false. For example,

Additional observer-like behavior may be exposed by the various integrations available. See below for more information on integrations.

Overriding instance / class methods

Hooking in behavior to the generated instance / class methods from the state machine, events, and states is very simple because of the way these methods are generated on the class. Using the class's ancestors, the original generated method can be referred to via super. For example,

class Vehicle
state_machine do
event :park do
...
end
end
def park(*args)
logger.info "..."
super
end
end

In the above example, the park instance method that's generated on the Vehicle class (by the associated event) is overridden with custom behavior. Once this behavior is complete, the original method from the state machine is invoked by simply calling super.

The same technique can be used for state, state_name, and all other instance and class methods on the Vehicle class.

Method conflicts

By default state_machine does not redefine methods that exist on superclasses (including Object) or any modules (including Kernel) that were included before it was defined. This is in order to ensure that existing behavior on the class is not broken by the inclusion of state_machine.

If a conflicting method is detected, state_machine will generate a warning. For example, consider the following class:

class Vehicle
state_machine do
event :open do
...
end
end
end

In the above class, an event named “open” is defined for its state machine. However, “open” is already defined as an instance method in Ruby's Kernel module that gets included in every Object. As a result, state_machine will generate the following warning:

Even though you may not be using Kernel's implementation of the “open” instance method, state_machine isn't aware of this and, as a result, stays safe and just skips redefining the method.

As with almost all helpers methods defined by state_machine in your class, there are generic methods available for working around this method conflict. In the example above, you can invoke the “open” event like so:

By default, state_machine helps prevent you from making mistakes and accidentally overriding methods that you didn't intend to. Once you understand this and what the consequences are, setting the ignore_method_conflicts option is a perfectly reasonable workaround.

Integrations

By default, state machines are library-agnostic, meaning that they work on any Ruby class and have no external dependencies. However, there are certain libraries which expose additional behavior that can be taken advantage of by state machines.

This library is built to work out of the box with a few popular Ruby libraries that allow for additional behavior to provide a cleaner and smoother experience. This is especially the case for objects backed by a database that may allow for transactions, persistent storage, search/filters, callbacks, etc.

When a state machine is defined for classes using any of the above libraries, it will try to automatically determine the integration to use (Agnostic, ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel) based on the class definition. To see how each integration affects the machine's behavior, refer to all constants defined under the StateMachines::Integrations namespace.

# File 'lib/state_machines/machine.rb', line 417deffind_or_create(owner_class,*args,&block)options=args.last.is_a?(Hash)?args.pop:{}name=args.first||:state# Find an existing machine
machine=owner_class.respond_to?(:state_machines)&&(args.first&&owner_class.state_machines[name]||!args.first&&owner_class.state_machines.values.first)||nilifmachine# Only create a new copy if changes are being made to the machine in
# a subclass
ifmachine.owner_class!=owner_class&&(options.any?||block_given?)machine=machine.clonemachine.initial_state=options[:initial]ifoptions.include?(:initial)machine.owner_class=owner_classend# Evaluate DSL
machine.instance_eval(&block)ifblock_given?else# No existing machine: create a new one
machine=new(owner_class,name,options,&block)endmachineend

#after_failure(*args, &block) ⇒ Object

Creates a callback that will be invoked after a transition failures to be performed so long as the given requirements match the transition.

See before_transition for a description of the possible configurations for defining callbacks. Note however that you cannot define the state requirements in these callbacks. You may only define event requirements.

The callback

Failure callbacks get invoked whenever an event fails to execute. This can happen when no transition is available, a before callback halts execution, or the action associated with this machine fails to succeed. In any of these cases, any failure callback that matches the attempted transition will be run.

#around_transition(*args, &block) ⇒ Object

Creates a callback that will be invoked around a transition so long as the given requirements match the transition.

The callback

Around callbacks wrap transitions, executing code both before and after. These callbacks are defined in the exact same manner as before / after callbacks with the exception that the transition must be yielded to in order to finish running it.

If defining around callbacks using blocks, you must yield within the transition by directly calling the block (since yielding is not allowed within blocks).

For example,

classVehiclestate_machinedoaround_transitiondo|block|Benchmark.measure{block.call}endaround_transitiondo|vehicle,block|logger.info"vehicle was #{state}..."block.calllogger.info"...and is now #{state}"endaround_transitiondo|vehicle,transition,block|logger.info"before #{transition.event}: #{vehicle.state}"block.calllogger.info"after #{transition.event}: #{vehicle.state}"endendend

Notice that referencing the block is similar to doing so within an actual method definition in that it is always the last argument.

On the other hand, if you're defining around callbacks using method references, you can yield like normal:

Event requirements

In addition to state requirements, an event requirement can be defined so that the callback is only invoked on specific events using the on option. This can also use the same matcher helpers as the state requirements.

Conditions

In addition to the state/event requirements, a condition can also be defined to help determine whether the callback should be invoked.

Configuration options:

:if - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.

:unless - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.

Accessing the transition

In addition to passing the object being transitioned, the actual transition describing the context (e.g. event, from, to) can be accessed as well. This additional argument is only passed if the callback allows for it.

For example,

classVehicle# Only specifies one parameter (the object being transitioned)
before_transitionall=>:parkeddo|vehicle|vehicle.set_alarmend# Specifies 2 parameters (object being transitioned and actual transition)
before_transitionall=>:parkeddo|vehicle,transition|vehicle.set_alarm(transition)endend

Note that the object in the callback will only be passed in as an argument if callbacks are configured to not be bound to the object involved. This is the default and may change on a per-integration basis.

See StateMachines::Transition for more information about the attributes available on the transition.

Usage with delegates

As noted above, state_machine uses the callback method's argument list arity to determine whether to include the transition in the method call. If you're using delegates, such as those defined in ActiveSupport or Forwardable, the actual arity of the delegated method gets masked. This means that callbacks which reference delegates will always get passed the transition as an argument. For example:

In the above example, Dashboard#refreshmust defined a transition argument. Otherwise, an ArgumentError exception will get raised. The only way around this is to avoid the use of delegates and manually define the delegate method so that the correct arity is used.

Examples

Below is an example of a class with one state machine and various types of before transitions defined for it:

#event(*names, &block) ⇒ ObjectAlso known as:
on

Defines one or more events for the machine and the transitions that can be performed when those events are run.

This method is also aliased as on for improved compatibility with using a domain-specific language.

Configuration options:

:human_name - The human-readable version of this event's name. By default, this is either defined by the integration or stringifies the name and converts underscores to spaces.

Instance methods

The following instance methods are generated when a new event is defined (the “park” event is used as an example):

park(..., run_action = true) - Fires the “park” event, transitioning from the current state to the next valid state. If the last argument is a boolean, it will control whether the machine's action gets run.

park!(..., run_action = true) - Fires the “park” event, transitioning from the current state to the next valid state. If the transition fails, then a StateMachines::InvalidTransition error will be raised. If the last argument is a boolean, it will control whether the machine's action gets run.

can_park?(requirements = {}) - Checks whether the “park” event can be fired given the current state of the object. This will not run validations or callbacks in ORM integrations. It will only determine if the state machine defines a valid transition for the event. To check whether an event can fire and passes validations, use event attributes (e.g. state_event) as described in the “Events” documentation of each ORM integration.

park_transition(requirements = {}) - Gets the next transition that would be performed if the “park” event were to be fired now on the object or nil if no transitions can be performed. Like can_park? this will also not run validations or callbacks. It will only determine if the state machine defines a valid transition for the event.

With a namespace of “car”, the above names map to the following methods:

can_park_car?

park_car_transition

park_car

park_car!

The can_park? and park_transition helpers both take an optional set of requirements for determining what transitions are available for the current object. These requirements include:

:from - One or more states to transition from. If none are specified, then this will be the object's current state.

:to - One or more states to transition to. If none are specified, then this will match any to state.

:guard - Whether to guard transitions with the if/unless conditionals defined for each one. Default is true.

Defining transitions

event requires a block which allows you to define the possible transitions that can happen as a result of that event. For example,

event:park,:stopdotransition:idling=>:parkedendevent:first_geardotransition:parked=>:first_gear,:if=>:seatbelt_on?transition:parked=>same# Allow to loopback if seatbelt is off
end

See StateMachines::Event#transition for more information on the possible options that can be passed in.

Note that this block is executed within the context of the actual event object. As a result, you will not be able to reference any class methods on the model without referencing the class itself. For example,

Overriding the event method

By default, this will define an instance method (with the same name as the event) that will fire the next possible transition for that. Although the before_transition, after_transition, and around_transition hooks allow you to define behavior that gets executed as a result of the event's transition, you can also override the event method in order to have a little more fine-grained control.

For example:

class Vehicle
state_machine do
event :park do
...
end
end
def park(*)
take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
if result = super # Runs the transition and all before/after/around hooks
applaud # Executes after the transition (and after_transition hooks)
end
result
end
end

There are a few important things to note here. First, the method signature is defined with an unlimited argument list in order to allow callers to continue passing arguments that are expected by state_machine. For example, it will still allow calls to park with a single parameter for skipping the configured action.

Second, the overridden event method must call super in order to run the logic for running the next possible transition. In order to remain consistent with other events, the result of super is returned.

Third, any behavior defined in this method will not get executed if you're taking advantage of attribute-based event transitions. For example:

vehicle=Vehicle.newvehicle.state_event='park'vehicle.save

In this case, the park event will run the before/after/around transition hooks and transition the state, but the behavior defined in the overriden park method will not be executed.

Defining additional arguments

Additional arguments can be passed into events and accessed by transition hooks like so:

If you decide to override the park event method and define additional arguments, you can do so as shown below:

class Vehicle
state_machine do
event :park do
...
end
end
def park(kind = :parallel, *args)
take_deep_breath if kind == :parallel
super
end
end

Note that super is called instead of super(*args). This allow the entire arguments list to be accessed by transition callbacks through StateMachines::Transition#args.

Using matchers

The all / any matchers can be used to easily execute blocks for a group of events. Note, however, that you cannot use these matchers to set configurations for events. Blocks using these matchers can be defined at any point in the state machine and will always get applied to the proper events.

Example

classVehiclestate_machinedo# The park, stop, and halt events will all share the given transitions
event:park,:stop,:haltdotransition[:idling,:backing_up]=>:parkedendevent:stopdotransition:first_gear=>:idlingendevent:ignitedotransition:parked=>:idlingtransition:idling=>same# Allow ignite while still idling
endendend

# File 'lib/state_machines/machine.rb', line 1307defevent(*names,&block)options=names.last.is_a?(Hash)?names.pop:{}options.assert_valid_keys(:human_name)# Store the context so that it can be used for / matched against any event
# that gets added
@events.context(names,&block)ifblock_given?ifnames.first.is_a?(Matcher)# Add any events referenced in the matcher. When matchers are used,
# events are not allowed to be configured.
raiseArgumentError,"Cannot configure events when using matchers (using #{options.inspect})"ifoptions.any?events=add_events(names.first.values)elseevents=add_events(names)# Update the configuration for the event(s)
events.eachdo|event|event.human_name=options[:human_name]ifoptions.include?(:human_name)# Add any states that may have been referenced within the event
add_states(event.known_states)endendevents.length==1?events.first:eventsend

#generate_message(name, values = []) ⇒ Object

Generates the message to use when invalidating the given object after failing to transition on a specific event

1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859

# File 'lib/state_machines/machine.rb', line 1849defgenerate_message(name,values=[])message=(@messages[name]||self.class.default_messages[name])# Check whether there are actually any values to interpolate to avoid
# any warnings
ifmessage.scan(/%./).any?{|match|match!='%%'}message%values.map{|value|value.last}elsemessageendend

#initial_state(object) ⇒ Object

Gets the initial state of the machine for the given object. If a dynamic initial state was configured for this machine, then the object will be passed into the lambda block to help determine the actual state.

# File 'lib/state_machines/machine.rb', line 593definitial_state=(new_initial_state)@initial_state=new_initial_stateadd_states([@initial_state])unlessdynamic_initial_state?# Update all states to reflect the new initial state
states.each{|state|state.initial=(state.name==@initial_state)}# Output a warning if there are conflicting initial states for the machine's
# attribute
initial_state=states.detect{|state|state.initial}if!owner_class_attribute_default.nil?&&(dynamic_initial_state?||!owner_class_attribute_default_matches?(initial_state))warn("Both #{owner_class.name} and its #{name.inspect} machine have defined "\
"a different default for \"#{attribute}\". Use only one or the other for "\
"defining defaults to avoid unexpected behaviors.")endend

#initialize_copy(orig) ⇒ Object

Creates a copy of this machine in addition to copies of each associated event/states/callback, so that the modifications to those collections do not affect the original machine.

#invalidate(_object, _attribute, _message, _values = []) ⇒ Object

#paths_for(object, requirements = {}) ⇒ Object

Generates a list of the possible transition sequences that can be run on the given object. These paths can reveal all of the possible states and events that can be encountered in the object's state machine based on the object's current state.

Configuration options:

from - The initial state to start all paths from. By default, this is the object's current state.

to - The target state to end all paths on. By default, paths will end when they loop back to the first transition on the path.

deep - Whether to allow the target state to be crossed more than once in a path. By default, paths will immediately stop when the target state (if specified) is reached. If this is enabled, then paths can continue even after reaching the target state; they will stop when reaching the target state a second time.

Note that the object is never modified when the list of paths is generated.

In addition to getting the possible paths that can be accessed, you can also get summary information about the states / events that can be accessed at some point along one of the paths. For example:

# Get the list of states that can be accessed from the current state
vehicle.state_paths.to_states# => [:idling, :first_gear, :second_gear]
# Get the list of events that can be accessed from the current state
vehicle.state_paths.events# => [:ignite, :shift_up, :shift_down]

In the above state machine, there are two states automatically discovered: :parked and :idling. These states, by default, will store their stringified equivalents when an object moves into that state (e.g. “parked” / “idling”).

For legacy systems or when tying state machines into existing frameworks, it's oftentimes necessary to need to store a different value for a state than the default. In order to continue taking advantage of an expressive state machine and helper methods, every defined state can be re-configured with a custom stored value. For example,

In the above example, each known state is configured to store it's associated database id in the state_id attribute. Also, notice that a lambda block is used to define the state's value. This is required in situations (like testing) where the model is loaded without any existing data (i.e. no VehicleState records available).

One caveat to the above example is to keep performance in mind. To avoid constant db hits for looking up the VehicleState ids, the value is cached by specifying the :cache option. Alternatively, a custom caching strategy can be used like so:

In the above definition, the :purchased state is customized with both a dynamic value and a value matcher.

When an object transitions to the purchased state, the value's lambda block will be called. This will get the current time and store it in the object's purchased_at attribute.

Note that the custom matcher is very important here. Since there's no way for the state machine to figure out an object's state when it's set to a runtime value, it must be explicitly defined. If the :if option were not configured for the state, then an ArgumentError exception would be raised at runtime, indicating that the state machine could not figure out what the current state of the object was.

Behaviors

Behaviors define a series of methods to mixin with objects when the current state matches the given one(s). This allows instance methods to behave a specific way depending on what the value of the object's state is.

Using matchers

The all / any matchers can be used to easily define behaviors for a group of states. Note, however, that you cannot use these matchers to set configurations for states. Behaviors using these matchers can be defined at any point in the state machine and will always get applied to the proper states.

For example:

state_machine :initial => :parked do
...
state all - [:parked, :idling, :stalled] do
validates_presence_of :speed
def speed
gear * 10
end
end
end

State-aware class methods

In addition to defining scopes for instance methods that are state-aware, the same can be done for certain types of class methods.

Some libraries have support for class-level methods that only run certain behaviors based on a conditions hash passed in. For example:

In the above ActiveRecord model, two validations have been defined which will only run when the Vehicle object is in one of the three states: first_gear, second_gear, or +third_gear. Notice, also, that if/unless conditions can continue to be used.

This functionality is not library-specific and can work for any class-level method that is defined like so:

def validates_presence_of(attribute, options = {})
...
end

The minimum requirement is that the last argument in the method be an options hash which contains at least :if condition support.

# File 'lib/state_machines/machine.rb', line 1005defstate(*names,&block)options=names.last.is_a?(Hash)?names.pop:{}options.assert_valid_keys(:value,:cache,:if,:human_name)# Store the context so that it can be used for / matched against any state
# that gets added
@states.context(names,&block)ifblock_given?ifnames.first.is_a?(Matcher)# Add any states referenced in the matcher. When matchers are used,
# states are not allowed to be configured.
raiseArgumentError,"Cannot configure states when using matchers (using #{options.inspect})"ifoptions.any?states=add_states(names.first.values)elsestates=add_states(names)# Update the configuration for the state(s)
states.eachdo|state|ifoptions.include?(:value)state.value=options[:value]self.states.update(state)endstate.human_name=options[:human_name]ifoptions.include?(:human_name)state.cache=options[:cache]ifoptions.include?(:cache)state.matcher=options[:if]ifoptions.include?(:if)endendstates.length==1?states.first:statesend

#transition(options) ⇒ Object

Creates a new transition that determines what to change the current state to when an event fires.

Defining transitions

The options for a new transition uses the Hash syntax to map beginning states to ending states. For example,

transition:parked=>:idling,:idling=>:first_gear,:on=>:ignite

In this case, when the ignite event is fired, this transition will cause the state to be idling if it's current state is parked or first_gear if it's current state is idling.

To help define these implicit transitions, a set of helpers are available for slightly more complex matching:

all - Matches every state in the machine

all - [:parked, :idling, ...] - Matches every state except those specified

any - An alias for all (matches every state in the machine)

same - Matches the same state being transitioned from

See StateMachines::MatcherHelpers for more information.

Examples:

transitionall=>nil,:on=>:ignite# Transitions to nil regardless of the current state
transitionall=>:idling,:on=>:ignite# Transitions to :idling regardless of the current state
transitionall-[:idling,:first_gear]=>:idling,:on=>:ignite# Transitions every state but :idling and :first_gear to :idling
transitionnil=>:idling,:on=>:ignite# Transitions to :idling from the nil state
transition:parked=>:idling,:on=>:ignite# Transitions to :idling if :parked
transition[:parked,:stalled]=>:idling,:on=>:ignite# Transitions to :idling if :parked or :stalled
transition:parked=>same,:on=>:park# Loops :parked back to :parked
transition[:parked,:stalled]=>same,:on=>[:park,:stall]# Loops either :parked or :stalled back to the same state on the park and stall events
transitionall-:parked=>same,:on=>:noop# Loops every state but :parked back to the same state
# Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
transition:parked=>:idling,:idling=>:first_gear,:first_gear=>:second_gear,:on=>:shift_up

Verbose transitions

Transitions can also be defined use an explicit set of configuration options:

:from - A state or array of states that can be transitioned from. If not specified, then the transition can occur for any state.

:to - The state that's being transitioned to. If not specified, then the transition will simply loop back (i.e. the state will not change).

:except_from - A state or array of states that cannot be transitioned from.

These options must be used when defining transitions within the context of a state.

Conditions

In addition to the state requirements for each transition, a condition can also be defined to help determine whether that transition is available. These options will work on both the normal and verbose syntax.

Configuration options:

:if - A method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}). The condition should return or evaluate to true or false.

:unless - A method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}). The condition should return or evaluate to true or false.