Class: StateMachines::Machine
- Inherits:
-
Object
- Object
- StateMachines::Machine
- Includes:
- EvalHelpers, MatcherHelpers
- Defined in:
- lib/state_machines/machine.rb
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.
For example,
class Vehicle
attr_accessor :fail, :saving_state
state_machine :initial => :parked, :action => :save do
event :ignite do
transition :parked => :idling
end
event :park do
transition :idling => :parked
end
end
def save
@saving_state = state
fail != true
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
vehicle.save # => true
vehicle.saving_state # => "parked" # The state was "parked" was save was called
# Successful event
vehicle.ignite # => true
vehicle.saving_state # => "idling" # The state was "idling" when save was called
vehicle.state # => "idling"
# Failed event
vehicle.fail = true
vehicle.park # => false
vehicle.saving_state # => "parked"
vehicle.state # => "idling"
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:
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
vehicle.state_event = 'ignite'
vehicle.save # => true
vehicle.state # => "idling"
vehicle.state_event # => nil
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:
def save
self.class.state_machines.transitions(self, :save).perform do
@saving_state = state
fail != true
end
end
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.
For example,
class Vehicle
state_machine initial: :parked do
after_transition all => :parked do
raise ArgumentError
end
...
end
end
vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
vehicle.save # => true (no exception raised)
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):class Vehicle state_machine initial: :pending after_transition pending: :parked, do: :track_initial_transition event :park do transition pending: :parked end state :pending, value: nil end end vehicle = Vehicle.new vehicle.park
-
Use a default event attribute that will automatically trigger when the configured action gets run (note that the
:pending
state is actually stored as nil):class Vehicle < ActiveRecord::Base state_machine initial: :pending after_transition pending: :parked, do: :track_initial_transition event :park do transition pending: :parked end state :pending, value: nil end def initialize(*) super self.state_event = 'park' end end vehicle = Vehicle.new vehicle.save
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,
class Vehicle
state_machine :initial => :parked do
before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
...
end
end
vehicle = Vehicle.new
vehicle.park # => false
vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling"
Observers
Observers, in the sense of external classes and not Ruby’s Observable mechanism, can hook into state machines as well. Such observers use the same callback api that’s used internally.
Below are examples of defining observers for the following state machine:
class Vehicle
state_machine do
event :park do
transition idling: :parked
end
...
end
...
end
Event/Transition behaviors:
class VehicleObserver
def self.before_park(vehicle, transition)
logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
end
def self.after_park(vehicle, transition, result)
logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
end
def self.before_transition(vehicle, transition)
logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
end
def self.after_transition(vehicle, transition)
logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
end
def self.around_transition(vehicle, transition)
logger.info Benchmark.measure { yield }
end
end
Vehicle.state_machine do
before_transition :on => :park, :do => VehicleObserver.method(:before_park)
before_transition VehicleObserver.method(:before_transition)
after_transition :on => :park, :do => VehicleObserver.method(:after_park)
after_transition VehicleObserver.method(:after_transition)
around_transition VehicleObserver.method(:around_transition)
end
One common callback is to record transitions for all models in the system for auditing/debugging purposes. Below is an example of an observer that can easily automate this process for all models:
class StateMachineObserver
def self.before_transition(object, transition)
Audit.log_transition(object.attributes)
end
end
[Vehicle, Switch, Project].each do |klass|
klass.state_machines.each do |attribute, machine|
machine.before_transition StateMachineObserver.method(:before_transition)
end
end
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:
Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
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:
vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
vehicle.fire_events(:open) # => true
# This will not work
vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
If you want to take on the risk of overriding existing methods and just ignore method conflicts altogether, you can do so by setting the following configuration:
StateMachines::Machine.ignore_method_conflicts = true
This will allow you to define events like “open” as described above and still generate the “open” instance helper method. For example:
StateMachines::Machine.ignore_method_conflicts = true
class Vehicle
state_machine do
event :open do
...
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
vehicle.open # => true
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.
Class Attribute Summary collapse
-
.default_messages ⇒ Object
Default messages to use for validation errors in ORM integrations.
-
.ignore_method_conflicts ⇒ Object
Returns the value of attribute ignore_method_conflicts.
Instance Attribute Summary collapse
-
#action ⇒ Object
readonly
The action to invoke when an object transitions.
-
#callbacks ⇒ Object
readonly
The callbacks to invoke before/after a transition is performed.
-
#events ⇒ Object
readonly
The events that trigger transitions.
-
#name ⇒ Object
readonly
The name of the machine, used for scoping methods generated for the machine as a whole (not states or events).
-
#namespace ⇒ Object
readonly
An identifier that forces all methods (including state predicates and event methods) to be generated with the value prefixed or suffixed, depending on the context.
-
#owner_class ⇒ Object
The class that the machine is defined in.
-
#states ⇒ Object
readonly
A list of all of the states known to this state machine.
-
#use_transactions ⇒ Object
readonly
Whether the machine will use transactions when firing events.
Class Method Summary collapse
- .draw ⇒ Object
-
.find_or_create(owner_class, *args, &block) ⇒ Object
Attempts to find or create a state machine for the given class.
Instance Method Summary collapse
-
#action_hook?(self_only = false) ⇒ Boolean
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
-
#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.
-
#after_transition(*args, &block) ⇒ Object
Creates a callback that will be invoked after a transition is performed so long as the given requirements match the transition.
-
#around_transition(*args, &block) ⇒ Object
Creates a callback that will be invoked around a transition so long as the given requirements match the transition.
-
#attribute(name = :state) ⇒ Object
Gets the actual name of the attribute on the machine’s owner class that stores data with the given name.
-
#before_transition(*args, &block) ⇒ Object
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
-
#define_helper(scope, method, *args, **kwargs, &block) ⇒ Object
Defines a new helper method in an instance or class scope with the given name.
- #draw ⇒ Object
-
#dynamic_initial_state? ⇒ Boolean
Whether a dynamic initial state is being used in the machine.
-
#errors_for(_object) ⇒ Object
Gets a description of the errors for the given object.
-
#event(*names, &block) ⇒ Object
(also: #on)
Defines one or more events for the machine and the transitions that can be performed when those events are run.
-
#generate_message(name, values = []) ⇒ Object
Generates the message to use when invalidating the given object after failing to transition on a specific event.
-
#initial_state(object) ⇒ Object
Gets the initial state of the machine for the given object.
-
#initial_state=(new_initial_state) ⇒ Object
Sets the initial state of the machine.
-
#initialize(owner_class, *args, &block) ⇒ Machine
constructor
Creates a new state machine for the given attribute.
-
#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.
-
#initialize_state(object, options = {}) ⇒ Object
Initializes the state on the given object.
-
#invalidate(_object, _attribute, _message, _values = []) ⇒ Object
Marks the given object as invalid with the given message.
-
#paths_for(object, requirements = {}) ⇒ Object
Generates a list of the possible transition sequences that can be run on the given object.
-
#read(object, attribute, ivar = false) ⇒ Object
Gets the current value stored in the given object’s attribute.
-
#reset(_object) ⇒ Object
Resets any errors previously added when invalidating the given object.
-
#state(*names, &block) ⇒ Object
(also: #other_states)
Customizes the definition of one or more states in the machine.
-
#transition(options) ⇒ Object
Creates a new transition that determines what to change the current state to when an event fires.
-
#within_transaction(object) ⇒ Object
Runs a transaction, rolling back any changes if the yielded block fails.
-
#write(object, attribute, value, ivar = false) ⇒ Object
Sets a new value in the given object’s attribute.
Methods included from MatcherHelpers
Methods included from EvalHelpers
Constructor Details
#initialize(owner_class, *args, &block) ⇒ Machine
Creates a new state machine for the given attribute
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/state_machines/machine.rb', line 503 def initialize(owner_class, *args, &block) = args.last.is_a?(Hash) ? args.pop : {} .assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions) # Find an integration that matches this machine's owner class if .include?(:integration) @integration = [:integration] && StateMachines::Integrations.find_by_name([:integration]) else @integration = StateMachines::Integrations.match(owner_class) end if @integration extend @integration = (@integration.defaults || {}).merge() end # Add machine-wide defaults = {use_transactions: true, initialize: true}.merge() # Set machine configuration @name = args.first || :state @attribute = [:attribute] || @name @events = EventCollection.new(self) @states = StateCollection.new(self) @callbacks = {before: [], after: [], failure: []} @namespace = [:namespace] @messages = [:messages] || {} @action = [:action] @use_transactions = [:use_transactions] @initialize_state = [:initialize] @action_hook_defined = false self.owner_class = owner_class # Merge with sibling machine configurations add_sibling_machine_configs # Define class integration define_helpers define_scopes([:plural]) after_initialize # Evaluate DSL instance_eval(&block) if block_given? self.initial_state = [:initial] unless sibling_machines.any? end |
Class Attribute Details
.default_messages ⇒ Object
Default messages to use for validation errors in ORM integrations
451 452 453 |
# File 'lib/state_machines/machine.rb', line 451 def @default_messages end |
.ignore_method_conflicts ⇒ Object
Returns the value of attribute ignore_method_conflicts.
452 453 454 |
# File 'lib/state_machines/machine.rb', line 452 def ignore_method_conflicts @ignore_method_conflicts end |
Instance Attribute Details
#action ⇒ Object (readonly)
The action to invoke when an object transitions
492 493 494 |
# File 'lib/state_machines/machine.rb', line 492 def action @action end |
#callbacks ⇒ Object (readonly)
The callbacks to invoke before/after a transition is performed
Maps :before => callbacks and :after => callbacks
489 490 491 |
# File 'lib/state_machines/machine.rb', line 489 def callbacks @callbacks end |
#events ⇒ Object (readonly)
The events that trigger transitions. These are sorted, by default, in the order in which they were defined.
473 474 475 |
# File 'lib/state_machines/machine.rb', line 473 def events @events end |
#name ⇒ Object (readonly)
The name of the machine, used for scoping methods generated for the machine as a whole (not states or events)
469 470 471 |
# File 'lib/state_machines/machine.rb', line 469 def name @name end |
#namespace ⇒ Object (readonly)
An identifier that forces all methods (including state predicates and event methods) to be generated with the value prefixed or suffixed, depending on the context.
497 498 499 |
# File 'lib/state_machines/machine.rb', line 497 def namespace @namespace end |
#owner_class ⇒ Object
The class that the machine is defined in
465 466 467 |
# File 'lib/state_machines/machine.rb', line 465 def owner_class @owner_class end |
#states ⇒ Object (readonly)
A list of all of the states known to this state machine. This will pull states from the following sources:
-
Initial state
-
State behaviors
-
Event transitions (:to, :from, and :except_from options)
-
Transition callbacks (:to, :from, :except_to, and :except_from options)
-
Unreferenced states (using
other_states
helper)
These are sorted, by default, in the order in which they were referenced.
484 485 486 |
# File 'lib/state_machines/machine.rb', line 484 def states @states end |
#use_transactions ⇒ Object (readonly)
Whether the machine will use transactions when firing events
500 501 502 |
# File 'lib/state_machines/machine.rb', line 500 def use_transactions @use_transactions end |
Class Method Details
.draw ⇒ Object
446 447 448 |
# File 'lib/state_machines/machine.rb', line 446 def draw(*) fail NotImplementedError end |
.find_or_create(owner_class, *args, &block) ⇒ Object
Attempts to find or create a state machine for the given class. For example,
StateMachines::Machine.find_or_create(Vehicle)
StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
StateMachines::Machine.find_or_create(Vehicle, :status)
StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
If a machine of the given name already exists in one of the class’s superclasses, then a copy of that machine will be created and stored in the new owner class (the original will remain unchanged).
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 |
# File 'lib/state_machines/machine.rb', line 417 def find_or_create(owner_class, *args, &block) = 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) || nil if machine # Only create a new copy if changes are being made to the machine in # a subclass if machine.owner_class != owner_class && (.any? || block_given?) machine = machine.clone machine.initial_state = [:initial] if .include?(:initial) machine.owner_class = owner_class end # Evaluate DSL machine.instance_eval(&block) if block_given? else # No existing machine: create a new one machine = new(owner_class, name, , &block) end machine end |
Instance Method Details
#action_hook?(self_only = false) ⇒ Boolean
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
1883 1884 1885 |
# File 'lib/state_machines/machine.rb', line 1883 def action_hook?(self_only = false) @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) } end |
#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.
For example,
class Vehicle
state_machine do
after_failure do |vehicle, transition|
logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
end
after_failure :on => :ignite, :do => :log_ignition_failure
...
end
end
1749 1750 1751 1752 1753 1754 1755 |
# File 'lib/state_machines/machine.rb', line 1749 def after_failure(*args, &block) = (args.last.is_a?(Hash) ? args.pop : {}) [:do] = args if args.any? .assert_valid_keys(:on, :do, :if, :unless) add_callback(:failure, , &block) end |
#after_transition(*args, &block) ⇒ Object
Creates a callback that will be invoked after a transition is performed so long as the given requirements match the transition.
See before_transition
for a description of the possible configurations for defining callbacks.
1654 1655 1656 1657 1658 |
# File 'lib/state_machines/machine.rb', line 1654 def after_transition(*args, &block) = (args.last.is_a?(Hash) ? args.pop : {}) [:do] = args if args.any? add_callback(:after, , &block) end |
#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,
class Vehicle
state_machine do
around_transition do |block|
Benchmark.measure { block.call }
end
around_transition do |vehicle, block|
logger.info "vehicle was #{state}..."
block.call
logger.info "...and is now #{state}"
end
around_transition do |vehicle, transition, block|
logger.info "before #{transition.event}: #{vehicle.state}"
block.call
logger.info "after #{transition.event}: #{vehicle.state}"
end
end
end
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:
class Vehicle
state_machine do
around_transition :benchmark
...
end
def benchmark
Benchmark.measure { yield }
end
end
See before_transition
for a description of the possible configurations for defining callbacks.
1715 1716 1717 1718 1719 |
# File 'lib/state_machines/machine.rb', line 1715 def around_transition(*args, &block) = (args.last.is_a?(Hash) ? args.pop : {}) [:do] = args if args.any? add_callback(:around, , &block) end |
#attribute(name = :state) ⇒ Object
Gets the actual name of the attribute on the machine’s owner class that stores data with the given name.
678 679 680 |
# File 'lib/state_machines/machine.rb', line 678 def attribute(name = :state) name == :state ? @attribute : :"#{self.name}_#{name}" end |
#before_transition(*args, &block) ⇒ Object
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
The callback
Callbacks must be defined as either an argument, in the :do option, or as a block. For example,
class Vehicle
state_machine do
before_transition :set_alarm
before_transition :set_alarm, all => :parked
before_transition all => :parked, :do => :set_alarm
before_transition all => :parked do |vehicle, transition|
vehicle.set_alarm
end
...
end
end
Notice that the first three callbacks are the same in terms of how the methods to invoke are defined. However, using the :do
can provide for a more fluid DSL.
In addition, multiple callbacks can be defined like so:
class Vehicle
state_machine do
before_transition :set_alarm, :lock_doors, all => :parked
before_transition all => :parked, :do => [:set_alarm, :lock_doors]
before_transition :set_alarm do |vehicle, transition|
vehicle.lock_doors
end
end
end
Notice that the different ways of configuring methods can be mixed.
State requirements
Callbacks can require that the machine be transitioning from and to specific states. These requirements use a Hash syntax to map beginning states to ending states. For example,
before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
In this case, the set_alarm
callback will only be called if the machine is transitioning from parked
to idling
or from idling
to parked
.
To help define state requirements, a set of helpers are available for slightly more complex matching:
-
all
- Matches every state/event in the machine -
all - [:parked, :idling, ...]
- Matches every state/event except those specified -
any
- An alias forall
(matches every state/event in the machine) -
same
- Matches the same state being transitioned from
See StateMachines::MatcherHelpers for more information.
Examples:
before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
before_transition all => :parked, :do => ... # Matches all states to parked
before_transition any => same, :do => ... # Matches every loopback
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.
Examples:
before_transition :on => :ignite, :do => ... # Matches only on ignite
before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
Verbose Requirements
Requirements can also be defined using verbose options rather than the implicit Hash syntax and helper methods described above.
Configuration options:
-
:from
- One or more states being transitioned from. If none are specified, then all states will match. -
:to
- One or more states being transitioned to. If none are specified, then all states will match. -
:on
- One or more events that fired the transition. If none are specified, then all events will match. -
:except_from
- One or more states not being transitioned from -
:except_to
- One more states not being transitioned to -
:except_on
- One or more events that *did not* fire the transition
Examples:
before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
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.
Examples:
before_transition :parked => :idling, :if => :moving?, :do => ...
before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
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,
class Vehicle
# Only specifies one parameter (the object being transitioned)
before_transition all => :parked do |vehicle|
vehicle.set_alarm
end
# Specifies 2 parameters (object being transitioned and actual transition)
before_transition all => :parked do |vehicle, transition|
vehicle.set_alarm(transition)
end
end
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:
class Vehicle
extend Forwardable
delegate :refresh => :dashboard
state_machine do
before_transition :refresh
...
end
def dashboard
@dashboard ||= Dashboard.new
end
end
class Dashboard
def refresh(transition)
# ...
end
end
In the above example, Dashboard#refresh
must 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:
class Vehicle
state_machine do
# Before all transitions
before_transition :update_dashboard
# Before specific transition:
before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
# With conditional callback:
before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
# Using helpers:
before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
...
end
end
As can be seen, any number of transitions can be created using various combinations of configuration options.
1643 1644 1645 1646 1647 |
# File 'lib/state_machines/machine.rb', line 1643 def before_transition(*args, &block) = (args.last.is_a?(Hash) ? args.pop : {}) [:do] = args if args.any? add_callback(:before, , &block) end |
#define_helper(scope, method, *args, **kwargs, &block) ⇒ Object
Defines a new helper method in an instance or class scope with the given name. If the method is already defined in the scope, then this will not override it.
If passing in a block, there are two side effects to be aware of
-
The method cannot be chained, meaning that the block cannot call
super
-
If the method is already defined in an ancestor, then it will not get overridden and a warning will be output.
Example:
# Instance helper
machine.define_helper(:instance, :state_name) do |machine, object|
machine.states.match(object).name
end
# Class helper
machine.define_helper(:class, :state_machine_name) do |machine, klass|
"State"
end
You can also define helpers using string evaluation like so:
# Instance helper
machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
def state_name
self.class.state_machine(:state).states.match(self).name
end
end_eval
# Class helper
machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
def state_machine_name
"State"
end
end_eval
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 |
# File 'lib/state_machines/machine.rb', line 718 def define_helper(scope, method, *args, **kwargs, &block) helper_module = @helper_modules.fetch(scope) if block_given? if !self.class.ignore_method_conflicts && (conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)) ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true." else name = self.name helper_module.class_eval do define_method(method) do |*block_args, **block_kwargs| block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args, **block_kwargs) end end end else helper_module.class_eval(method, *args, **kwargs) end end |
#draw ⇒ Object
1877 1878 1879 |
# File 'lib/state_machines/machine.rb', line 1877 def draw(*) fail NotImplementedError end |
#dynamic_initial_state? ⇒ Boolean
Whether a dynamic initial state is being used in the machine
651 652 653 |
# File 'lib/state_machines/machine.rb', line 651 def dynamic_initial_state? instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc) end |
#errors_for(_object) ⇒ Object
Gets a description of the errors for the given object. This is used to provide more detailed information when an InvalidTransition exception is raised.
1839 1840 1841 |
# File 'lib/state_machines/machine.rb', line 1839 def errors_for(_object) '' end |
#event(*names, &block) ⇒ Object Also 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. Likecan_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, :stop do
transition :idling => :parked
end
event :first_gear do
transition :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,
class Vehicle
def self.safe_states
[:parked, :idling, :stalled]
end
state_machine do
event :park do
transition Vehicle.safe_states => :parked
end
end
end
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.new
vehicle.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:
class Vehicle
state_machine do
after_transition :on => :park do |vehicle, transition|
kind = *transition.args # :parallel
...
end
after_transition :on => :park, :do => :take_deep_breath
event :park do
...
end
def take_deep_breath(transition)
kind = *transition.args # :parallel
...
end
end
end
vehicle = Vehicle.new
vehicle.park(:parallel)
Remember that if the last argument is a boolean, it will be used as the run_action
parameter to the event action. Using the park
action example from above, you can might call it like so:
vehicle.park # => Uses default args and runs machine action
vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
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.
For example:
state_machine :initial => :parked do
...
event all - [:crash] do
transition :stalled => :parked
end
end
Example
class Vehicle
state_machine do
# The park, stop, and halt events will all share the given transitions
event :park, :stop, :halt do
transition [:idling, :backing_up] => :parked
end
event :stop do
transition :first_gear => :idling
end
event :ignite do
transition :parked => :idling
transition :idling => same # Allow ignite while still idling
end
end
end
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 |
# File 'lib/state_machines/machine.rb', line 1308 def event(*names, &block) = names.last.is_a?(Hash) ? names.pop : {} .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) if block_given? if names.first.is_a?(Matcher) # Add any events referenced in the matcher. When matchers are used, # events are not allowed to be configured. raise ArgumentError, "Cannot configure events when using matchers (using #{.inspect})" if .any? events = add_events(names.first.values) else events = add_events(names) # Update the configuration for the event(s) events.each do |event| event.human_name = [:human_name] if .include?(:human_name) # Add any states that may have been referenced within the event add_states(event.known_states) end end events.length == 1 ? events.first : events end |
#generate_message(name, values = []) ⇒ Object
Generates the message to use when invalidating the given object after failing to transition on a specific event
1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 |
# File 'lib/state_machines/machine.rb', line 1851 def (name, values = []) = (@messages[name] || self.class.[name]) # Check whether there are actually any values to interpolate to avoid # any warnings if .scan(/%./).any? { |match| match != '%%' } % values.map { |value| value.last } else end end |
#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.
Examples
With a static initial state:
class Vehicle
state_machine :initial => :parked do
...
end
end
vehicle = Vehicle.new
Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
With a dynamic initial state:
class Vehicle
attr_accessor :force_idle
state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
...
end
end
vehicle = Vehicle.new
vehicle.force_idle = true
Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
vehicle.force_idle = false
Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646 647 648 |
# File 'lib/state_machines/machine.rb', line 646 def initial_state(object) states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state') end |
#initial_state=(new_initial_state) ⇒ Object
Sets the initial state of the machine. This can be either the static name of a state or a lambda block which determines the initial state at creation time.
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 |
# File 'lib/state_machines/machine.rb', line 593 def initial_state=(new_initial_state) @initial_state = new_initial_state add_states([@initial_state]) unless dynamic_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." ) end end |
#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.
552 553 554 555 556 557 558 559 560 |
# File 'lib/state_machines/machine.rb', line 552 def initialize_copy(orig) #:nodoc: super @events = @events.dup @events.machine = self @states = @states.dup @states.machine = self @callbacks = {before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup} end |
#initialize_state(object, options = {}) ⇒ Object
Initializes the state on the given object. Initial values are only set if the machine’s attribute hasn’t been previously initialized.
Configuration options:
-
:force
- Whether to initialize the state regardless of its current value -
:to
- A hash to set the initial value in instead of writing directly to the object
663 664 665 666 667 668 669 670 671 672 673 674 |
# File 'lib/state_machines/machine.rb', line 663 def initialize_state(object, = {}) state = initial_state(object) if state && ([:force] || initialize_state?(object)) value = state.value if (hash = [:to]) hash[attribute.to_s] = value else write(object, :state, value) end end end |
#invalidate(_object, _attribute, _message, _values = []) ⇒ Object
Marks the given object as invalid with the given message.
By default, this is a no-op.
1833 1834 |
# File 'lib/state_machines/machine.rb', line 1833 def invalidate(_object, _attribute, , _values = []) end |
#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.
Examples
class Vehicle
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
event :shift_up do
transition :idling => :first_gear, :first_gear => :second_gear
end
event :shift_down do
transition :second_gear => :first_gear, :first_gear => :idling
end
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
vehicle.state # => "parked"
vehicle.state_paths
# => [
# [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
# #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
# #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
#
# [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
# ]
vehicle.state_paths(:from => :parked, :to => :second_gear)
# => [
# [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
# ]
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]
1826 1827 1828 |
# File 'lib/state_machines/machine.rb', line 1826 def paths_for(object, requirements = {}) PathCollection.new(object, self, requirements) end |
#read(object, attribute, ivar = false) ⇒ Object
Gets the current value stored in the given object’s attribute.
For example,
class Vehicle
state_machine :initial => :parked do
...
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1053 1054 1055 1056 1057 1058 1059 1060 |
# File 'lib/state_machines/machine.rb', line 1053 def read(object, attribute, ivar = false) attribute = self.attribute(attribute) if ivar object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil else object.send(attribute) end end |
#reset(_object) ⇒ Object
Resets any errors previously added when invalidating the given object.
By default, this is a no-op.
1846 1847 |
# File 'lib/state_machines/machine.rb', line 1846 def reset(_object) end |
#state(*names, &block) ⇒ Object Also known as: other_states
Customizes the definition of one or more states in the machine.
Configuration options:
-
:value
- The actual value to store when an object transitions to the state. Default is the name (stringified). -
:cache
- If a dynamic value (via a lambda block) is being used, then setting this to true will cache the evaluated result -
:if
- Determines whether an object’s value matches the state (e.g. :value => lambda Time.now, :if => lambda {|state| !state.nil?}). By default, the configured value is matched. -
:human_name
- The human-readable version of this state’s name. By default, this is either defined by the integration or stringifies the name and converts underscores to spaces.
Customizing the stored value
Whenever a state is automatically discovered in the state machine, its default value is assumed to be the stringified version of the name. For example,
class Vehicle
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
end
end
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,
class Vehicle
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
state :idling, :value => 'IDLING'
state :parked, :value => 'PARKED
end
end
This is also useful if being used in association with a database and, instead of storing the state name in a column, you want to store the state’s foreign key:
class VehicleState < ActiveRecord::Base
end
class Vehicle < ActiveRecord::Base
state_machine :attribute => :state_id, :initial => :parked do
event :ignite do
transition :parked => :idling
end
states.each do |state|
self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
end
end
end
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:
class VehicleState < ActiveRecord::Base
cattr_accessor :cache_store
self.cache_store = ActiveSupport::Cache::MemoryStore.new
def self.find_by_name(name)
cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
end
end
Dynamic values
In addition to customizing states with other value types, lambda blocks can also be specified to allow for a state’s value to be determined dynamically at runtime. For example,
class Vehicle
state_machine :purchased_at, :initial => :available do
event :purchase do
transition all => :purchased
end
event :restock do
transition all => :available
end
state :available, :value => nil
state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
end
end
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.
For example,
class Vehicle
attr_accessor :driver
attr_accessor :passenger
state_machine :initial => :parked do
event :ignite do
transition :parked => :idling
end
state :parked do
def speed
0
end
def rotate_driver
driver = self.driver
self.driver = passenger
self.passenger = driver
true
end
end
state :idling, :first_gear do
def speed
20
end
def rotate_driver
self.state = 'parked'
rotate_driver
end
end
other_states :backing_up
end
end
In the above example, there are two dynamic behaviors defined for the class:
-
speed
-
rotate_driver
Each of these behaviors are instance methods on the Vehicle class. However, which method actually gets invoked is based on the current state of the object. Using the above class as the example:
vehicle = Vehicle.new
vehicle.driver = 'John'
vehicle.passenger = 'Jane'
# Behaviors in the "parked" state
vehicle.state # => "parked"
vehicle.speed # => 0
vehicle.rotate_driver # => true
vehicle.driver # => "Jane"
vehicle.passenger # => "John"
vehicle.ignite # => true
# Behaviors in the "idling" state
vehicle.state # => "idling"
vehicle.speed # => 20
vehicle.rotate_driver # => true
vehicle.driver # => "John"
vehicle.passenger # => "Jane"
As can be seen, both the speed
and rotate_driver
instance method implementations changed how they behave based on what the current state of the vehicle was.
Invalid behaviors
If a specific behavior has not been defined for a state, then a NoMethodError exception will be raised, indicating that that method would not normally exist for an object with that state.
Using the example from before:
vehicle = Vehicle.new
vehicle.state = 'backing_up'
vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
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:
class Vehicle < ActiveRecord::Base
state_machine do
...
state :first_gear, :second_gear, :third_gear do
validates_presence_of :speed
validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
end
end
end
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.
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 |
# File 'lib/state_machines/machine.rb', line 1005 def state(*names, &block) = names.last.is_a?(Hash) ? names.pop : {} .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) if block_given? if names.first.is_a?(Matcher) # Add any states referenced in the matcher. When matchers are used, # states are not allowed to be configured. raise ArgumentError, "Cannot configure states when using matchers (using #{.inspect})" if .any? states = add_states(names.first.values) else states = add_states(names) # Update the configuration for the state(s) states.each do |state| if .include?(:value) state.value = [:value] self.states.update(state) end state.human_name = [:human_name] if .include?(:human_name) state.cache = [:cache] if .include?(:cache) state.matcher = [:if] if .include?(:if) end end states.length == 1 ? states.first : states end |
#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 forall
(matches every state in the machine) -
same
- Matches the same state being transitioned from
See StateMachines::MatcherHelpers for more information.
Examples:
transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
transition nil => :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
transition all - :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.
Examples:
transition :to => nil, :on => :park
transition :to => :idling, :on => :ignite
transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
transition :from => nil, :to => :idling, :on => :ignite
transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
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.
Examples:
transition :parked => :idling, :on => :ignite, :if => :moving?
transition :parked => :idling, :on => :ignite, :unless => :stopped?
transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
Order of operations
Transitions are evaluated in the order in which they’re defined. As a result, if more than one transition applies to a given object, then the first transition that matches will be performed.
1428 1429 1430 1431 1432 1433 1434 1435 1436 |
# File 'lib/state_machines/machine.rb', line 1428 def transition() raise ArgumentError, 'Must specify :on event' unless [:on] branches = [] = .dup event(*Array(.delete(:on))) { branches << transition() } branches.length == 1 ? branches.first : branches end |
#within_transaction(object) ⇒ Object
Runs a transaction, rolling back any changes if the yielded block fails.
This is only applicable to integrations that involve databases. By default, this will not run any transactions since the changes aren’t taking place within the context of a database.
1868 1869 1870 1871 1872 1873 1874 |
# File 'lib/state_machines/machine.rb', line 1868 def within_transaction(object) if use_transactions transaction(object) { yield } else yield end end |
#write(object, attribute, value, ivar = false) ⇒ Object
Sets a new value in the given object’s attribute.
For example,
class Vehicle
state_machine :initial => :parked do
...
end
end
vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
vehicle.state # => "idling"
vehicle.event # => "park"
1077 1078 1079 1080 |
# File 'lib/state_machines/machine.rb', line 1077 def write(object, attribute, value, ivar = false) attribute = self.attribute(attribute) ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value) end |