| Class | StateMachine::Machine |
| In: |
lib/state_machine/machine.rb
|
| Parent: | Object |
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.
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.
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 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 StateMachine::Machine#before_transition and StateMachine::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:
class Vehicle
before :create, :track_initial_transition
state_machine do
...
end
end
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
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
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! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
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.
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.
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 StateMachine::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:
StateMachine::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:
StateMachine::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.
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 StateMachine::Integrations namespace.
| action | [R] | The action to invoke when an object transitions |
| callbacks | [R] |
The callbacks to invoke before/after a transition is performed
Maps :before => callbacks and :after => callbacks |
| default_messages | [RW] | |
| events | [R] | The events that trigger transitions. These are sorted, by default, in the order in which they were defined. |
| ignore_method_conflicts | [RW] | |
| name | [R] | The name of the machine, used for scoping methods generated for the machine as a whole (not states or events) |
| namespace | [R] | 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 | [RW] | The class that the machine is defined in |
| states | [R] |
A list of all of the states known to this state machine. This will pull states from
the following sources:
These are sorted, by default, in the order in which they were referenced. |
| use_transactions | [R] | Whether the machine will use transactions when firing events |
Draws the state machines defined in the given classes using GraphViz. The given classes must be a comma-delimited string of class names.
Configuration options:
# File lib/state_machine/machine.rb, line 464
464: def draw(class_names, options = {})
465: raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
466:
467: # Load any files
468: if files = options.delete(:file)
469: files.split(',').each {|file| require file}
470: end
471:
472: class_names.split(',').each do |class_name|
473: # Navigate through the namespace structure to get to the class
474: klass = Object
475: class_name.split('::').each do |name|
476: klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
477: end
478:
479: # Draw each of the class's state machines
480: klass.state_machines.each_value do |machine|
481: machine.draw(options)
482: end
483: end
484: end
Attempts to find or create a state machine for the given class. For example,
StateMachine::Machine.find_or_create(Vehicle) StateMachine::Machine.find_or_create(Vehicle, :initial => :parked) StateMachine::Machine.find_or_create(Vehicle, :status) StateMachine::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).
# File lib/state_machine/machine.rb, line 431
431: def find_or_create(owner_class, *args, &block)
432: options = args.last.is_a?(Hash) ? args.pop : {}
433: name = args.first || :state
434:
435: # Find an existing machine
436: if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
437: # Only create a new copy if changes are being made to the machine in
438: # a subclass
439: if machine.owner_class != owner_class && (options.any? || block_given?)
440: machine = machine.clone
441: machine.initial_state = options[:initial] if options.include?(:initial)
442: machine.owner_class = owner_class
443: end
444:
445: # Evaluate DSL
446: machine.instance_eval(&block) if block_given?
447: else
448: # No existing machine: create a new one
449: machine = new(owner_class, name, options, &block)
450: end
451:
452: machine
453: end
Creates a new state machine for the given attribute
# File lib/state_machine/machine.rb, line 539
539: def initialize(owner_class, *args, &block)
540: options = args.last.is_a?(Hash) ? args.pop : {}
541: assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
542:
543: # Find an integration that matches this machine's owner class
544: if options.include?(:integration)
545: @integration = StateMachine::Integrations.find_by_name(options[:integration]) if options[:integration]
546: else
547: @integration = StateMachine::Integrations.match(owner_class)
548: end
549:
550: if @integration
551: extend @integration
552: options = (@integration.defaults || {}).merge(options)
553: end
554:
555: # Add machine-wide defaults
556: options = {:use_transactions => true, :initialize => true}.merge(options)
557:
558: # Set machine configuration
559: @name = args.first || :state
560: @attribute = options[:attribute] || @name
561: @events = EventCollection.new(self)
562: @states = StateCollection.new(self)
563: @callbacks = {:before => [], :after => [], :failure => []}
564: @namespace = options[:namespace]
565: @messages = options[:messages] || {}
566: @action = options[:action]
567: @use_transactions = options[:use_transactions]
568: @initialize_state = options[:initialize]
569: self.owner_class = owner_class
570: self.initial_state = options[:initial] unless sibling_machines.any?
571:
572: # Merge with sibling machine configurations
573: add_sibling_machine_configs
574:
575: # Define class integration
576: define_helpers
577: define_scopes(options[:plural])
578: after_initialize
579:
580: # Evaluate DSL
581: instance_eval(&block) if block_given?
582: end
Determines whether an action hook was defined for firing attribute-based event transitions when the configured action gets called.
# File lib/state_machine/machine.rb, line 1917
1917: def action_hook?(self_only = false)
1918: @action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)}
1919: end
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.
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
# File lib/state_machine/machine.rb, line 1730
1730: def after_failure(*args, &block)
1731: options = (args.last.is_a?(Hash) ? args.pop : {})
1732: options[:do] = args if args.any?
1733: assert_valid_keys(options, :on, :do, :if, :unless)
1734:
1735: add_callback(:failure, options, &block)
1736: end
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.
# File lib/state_machine/machine.rb, line 1635
1635: def after_transition(*args, &block)
1636: options = (args.last.is_a?(Hash) ? args.pop : {})
1637: options[:do] = args if args.any?
1638: add_callback(:after, options, &block)
1639: end
Creates a callback that will be invoked around a transition so long as the given requirements match the transition.
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.
# File lib/state_machine/machine.rb, line 1696
1696: def around_transition(*args, &block)
1697: options = (args.last.is_a?(Hash) ? args.pop : {})
1698: options[:do] = args if args.any?
1699: add_callback(:around, options, &block)
1700: end
Creates a callback that will be invoked before a transition is performed so long as the given requirements match the transition.
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.
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:
See StateMachine::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
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
Requirements can also be defined using verbose options rather than the implicit Hash syntax and helper methods described above.
Configuration options:
Examples:
before_transition :from => :ignite, :to => :idling, :on => :park, :do => ... before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
In addition to the state/event requirements, a condition can also be defined to help determine whether the callback should be invoked.
Configuration options:
Examples:
before_transition :parked => :idling, :if => :moving?, :do => ... before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
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 StateMachine::Transition for more information about the attributes available on the transition.
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.
# File lib/state_machine/machine.rb, line 1624
1624: def before_transition(*args, &block)
1625: options = (args.last.is_a?(Hash) ? args.pop : {})
1626: options[:do] = args if args.any?
1627: add_callback(:before, options, &block)
1628: end
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
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
# File lib/state_machine/machine.rb, line 742
742: def define_helper(scope, method, *args, &block)
743: helper_module = @helper_modules.fetch(scope)
744:
745: if block_given?
746: if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
747: ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
748: warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true."
749: else
750: name = self.name
751: helper_module.class_eval do
752: define_method(method) do |*args|
753: block.call((scope == :instance ? self.class : self).state_machine(name), self, *args)
754: end
755: end
756: end
757: else
758: helper_module.class_eval(method, *args)
759: end
760: end
Draws a directed graph of the machine for visualizing the various events, states, and their transitions.
This requires both the Ruby graphviz gem and the graphviz library be installed on the system.
Configuration options:
# File lib/state_machine/machine.rb, line 1867
1867: def draw(options = {})
1868: options = {
1869: :name => "#{owner_class.name}_#{name}",
1870: :path => '.',
1871: :format => 'png',
1872: :font => 'Arial',
1873: :orientation => 'portrait'
1874: }.merge(options)
1875: assert_valid_keys(options, :name, :path, :format, :font, :orientation)
1876:
1877: begin
1878: # Load the graphviz library
1879: require 'rubygems'
1880: gem 'ruby-graphviz', '>=0.9.0'
1881: require 'graphviz'
1882:
1883: graph = GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB')
1884:
1885: # Add nodes
1886: states.by_priority.each do |state|
1887: node = state.draw(graph)
1888: node.fontname = options[:font]
1889: end
1890:
1891: # Add edges
1892: events.each do |event|
1893: edges = event.draw(graph)
1894: edges.each {|edge| edge.fontname = options[:font]}
1895: end
1896:
1897: # Generate the graph
1898: graphvizVersion = Constants::RGV_VERSION.split('.')
1899: file = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
1900:
1901: if graphvizVersion[0] == '0' && graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
1902: outputOptions = {:output => options[:format], :file => file}
1903: else
1904: outputOptions = {options[:format] => file}
1905: end
1906:
1907: graph.output(outputOptions)
1908: graph
1909: rescue LoadError => ex
1910: $stderr.puts "Cannot draw the machine (#{ex.message}). `gem install ruby-graphviz` >= v0.9.0 and try again."
1911: false
1912: end
1913: end
Gets a description of the errors for the given object. This is used to provide more detailed information when an InvalidTransition exception is raised.
# File lib/state_machine/machine.rb, line 1820
1820: def errors_for(object)
1821: ''
1822: end
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:
The following instance methods are generated when a new event is defined (the "park" event is used as an example):
With a namespace of "car", the above names map to the following methods:
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:
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 StateMachine::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
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.
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 StateMachine::Transition#args.
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
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
# File lib/state_machine/machine.rb, line 1326
1326: def event(*names, &block)
1327: options = names.last.is_a?(Hash) ? names.pop : {}
1328: assert_valid_keys(options, :human_name)
1329:
1330: # Store the context so that it can be used for / matched against any event
1331: # that gets added
1332: @events.context(names, &block) if block_given?
1333:
1334: if names.first.is_a?(Matcher)
1335: # Add any events referenced in the matcher. When matchers are used,
1336: # events are not allowed to be configured.
1337: raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1338: events = add_events(names.first.values)
1339: else
1340: events = add_events(names)
1341:
1342: # Update the configuration for the event(s)
1343: events.each do |event|
1344: event.human_name = options[:human_name] if options.include?(:human_name)
1345:
1346: # Add any states that may have been referenced within the event
1347: add_states(event.known_states)
1348: end
1349: end
1350:
1351: events.length == 1 ? events.first : events
1352: end
Generates the message to use when invalidating the given object after failing to transition on a specific event
# File lib/state_machine/machine.rb, line 1832
1832: def generate_message(name, values = [])
1833: (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1834: end
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.
With a static initial state:
class Vehicle
state_machine :initial => :parked do
...
end
end
vehicle = Vehicle.new
Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::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) # => #<StateMachine::State name=:idling value="idling" initial=false>
vehicle.force_idle = false
Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
# File lib/state_machine/machine.rb, line 670
670: def initial_state(object)
671: states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
672: end
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.
# File lib/state_machine/machine.rb, line 628
628: def initial_state=(new_initial_state)
629: @initial_state = new_initial_state
630: add_states([@initial_state]) unless dynamic_initial_state?
631:
632: # Update all states to reflect the new initial state
633: states.each {|state| state.initial = (state.name == @initial_state)}
634: end
Initializes the state on the given object. Initial values are only set if the machine‘s attribute hasn‘t been previously initialized.
Configuration options:
# File lib/state_machine/machine.rb, line 687
687: def initialize_state(object, options = {})
688: state = initial_state(object)
689: if state && (options[:force] || initialize_state?(object))
690: value = state.value
691:
692: if hash = options[:to]
693: hash[attribute.to_s] = value
694: else
695: write(object, :state, value)
696: end
697: end
698: end
Marks the given object as invalid with the given message.
By default, this is a no-op.
# File lib/state_machine/machine.rb, line 1814
1814: def invalidate(object, attribute, message, values = [])
1815: end
Sets the class which is the owner of this state machine. Any methods generated by states, events, or other parts of the machine will be defined on the given owner class.
# File lib/state_machine/machine.rb, line 600
600: def owner_class=(klass)
601: @owner_class = klass
602:
603: # Create modules for extending the class with state/event-specific methods
604: @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
605: owner_class.class_eval do
606: extend helper_modules[:class]
607: include helper_modules[:instance]
608: end
609:
610: # Add class-/instance-level methods to the owner class for state initialization
611: unless owner_class < StateMachine::InstanceMethods
612: owner_class.class_eval do
613: extend StateMachine::ClassMethods
614: include StateMachine::InstanceMethods
615: end
616:
617: define_state_initializer if @initialize_state
618: end
619:
620: # Record this machine as matched to the name in the current owner class.
621: # This will override any machines mapped to the same name in any superclasses.
622: owner_class.state_machines[name] = self
623: end
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:
Note that the object is never modified when the list of paths is generated.
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
# => [
# [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
# #<StateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
# #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
#
# [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachine::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)
# => [
# [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
# #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
# #<StateMachine::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]
# File lib/state_machine/machine.rb, line 1807
1807: def paths_for(object, requirements = {})
1808: PathCollection.new(object, self, requirements)
1809: end
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
# File lib/state_machine/machine.rb, line 1075
1075: def read(object, attribute, ivar = false)
1076: attribute = self.attribute(attribute)
1077: ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
1078: end
Resets any errors previously added when invalidating the given object.
By default, this is a no-op.
# File lib/state_machine/machine.rb, line 1827
1827: def reset(object)
1828: end
Customizes the definition of one or more states in the machine.
Configuration options:
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
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 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:
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.
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"
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
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.
# File lib/state_machine/machine.rb, line 1029
1029: def state(*names, &block)
1030: options = names.last.is_a?(Hash) ? names.pop : {}
1031: assert_valid_keys(options, :value, :cache, :if, :human_name)
1032:
1033: # Store the context so that it can be used for / matched against any state
1034: # that gets added
1035: @states.context(names, &block) if block_given?
1036:
1037: if names.first.is_a?(Matcher)
1038: # Add any states referenced in the matcher. When matchers are used,
1039: # states are not allowed to be configured.
1040: raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1041: states = add_states(names.first.values)
1042: else
1043: states = add_states(names)
1044:
1045: # Update the configuration for the state(s)
1046: states.each do |state|
1047: if options.include?(:value)
1048: state.value = options[:value]
1049: self.states.update(state)
1050: end
1051:
1052: state.human_name = options[:human_name] if options.include?(:human_name)
1053: state.cache = options[:cache] if options.include?(:cache)
1054: state.matcher = options[:if] if options.include?(:if)
1055: end
1056: end
1057:
1058: states.length == 1 ? states.first : states
1059: end
Creates a new transition that determines what to change the current state to when an event fires.
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:
See StateMachine::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
Transitions can also be defined use an explicit set of configuration options:
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
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:
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?
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.
# File lib/state_machine/machine.rb, line 1444
1444: def transition(options)
1445: raise ArgumentError, 'Must specify :on event' unless options[:on]
1446:
1447: branches = []
1448: event(*Array(options.delete(:on))) { branches << transition(options) }
1449:
1450: branches.length == 1 ? branches.first : branches
1451: end
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.
# File lib/state_machine/machine.rb, line 1841
1841: def within_transaction(object)
1842: if use_transactions
1843: transaction(object) { yield }
1844: else
1845: yield
1846: end
1847: end
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"
# File lib/state_machine/machine.rb, line 1095
1095: def write(object, attribute, value, ivar = false)
1096: attribute = self.attribute(attribute)
1097: ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1098: end
The method to hook into for triggering transitions when invoked. By default, this is the action configured for the machine.
Since the default hook technique relies on module inheritance, the action must be defined in an ancestor of the owner classs in order for it to be the action hook.
# File lib/state_machine/machine.rb, line 2084
2084: def action_hook
2085: action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2086: end
Adds a new transition callback of the given type.
# File lib/state_machine/machine.rb, line 2211
2211: def add_callback(type, options, &block)
2212: callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2213: add_states(callback.known_states)
2214: callback
2215: end
Tracks the given set of events in the list of all known events for this machine
# File lib/state_machine/machine.rb, line 2240
2240: def add_events(new_events)
2241: new_events.map do |new_event|
2242: # Check for other states that use a different class type for their name.
2243: # This typically prevents string / symbol misuse.
2244: if conflict = events.detect {|event| event.name.class != new_event.class}
2245: raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2246: end
2247:
2248: unless event = events[new_event]
2249: events << event = Event.new(self, new_event)
2250: end
2251:
2252: event
2253: end
2254: end
Updates this machine based on the configuration of other machines in the owner class that share the same target attribute.
# File lib/state_machine/machine.rb, line 2203
2203: def add_sibling_machine_configs
2204: # Add existing states
2205: sibling_machines.each do |machine|
2206: machine.states.each {|state| states << state unless states[state.name]}
2207: end
2208: end
Tracks the given set of states in the list of all known states for this machine
# File lib/state_machine/machine.rb, line 2219
2219: def add_states(new_states)
2220: new_states.map do |new_state|
2221: # Check for other states that use a different class type for their name.
2222: # This typically prevents string / symbol misuse.
2223: if new_state && conflict = states.detect {|state| state.name && state.name.class != new_state.class}
2224: raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2225: end
2226:
2227: unless state = states[new_state]
2228: states << state = State.new(self, new_state)
2229:
2230: # Copy states over to sibling machines
2231: sibling_machines.each {|machine| machine.states << state}
2232: end
2233:
2234: state
2235: end
2236: end
Runs additional initialization hooks. By default, this is a no-op.
# File lib/state_machine/machine.rb, line 1923
1923: def after_initialize
1924: end
Adds helper methods for automatically firing events when an action is invoked
# File lib/state_machine/machine.rb, line 2052
2052: def define_action_helpers
2053: if action_hook
2054: @action_hook_defined = true
2055: define_action_hook
2056: end
2057: end
Determines whether action helpers should be defined for this machine. This is only true if there is an action configured and no other machines have process this same configuration already.
# File lib/state_machine/machine.rb, line 2046
2046: def define_action_helpers?
2047: action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
2048: end
Hooks directly into actions by defining the same method in an included module. As a result, when the action gets invoked, any state events defined for the object will get run. Method visibility is preserved.
# File lib/state_machine/machine.rb, line 2062
2062: def define_action_hook
2063: action_hook = self.action_hook
2064: action = self.action
2065: private_action_hook = owner_class.private_method_defined?(action_hook)
2066:
2067: # Only define helper if it hasn't
2068: define_helper :instance, "def \#{action_hook}(*)\nself.class.state_machines.transitions(self, \#{action.inspect}).perform { super }\nend\n\nprivate \#{action_hook.inspect} if \#{private_action_hook}\n", __FILE__, __LINE__ + 1
2069: end
Adds helper methods for getting information about this state machine‘s events
# File lib/state_machine/machine.rb, line 1993
1993: def define_event_helpers
1994: # Gets the events that are allowed to fire on the current object
1995: define_helper(:instance, attribute(:events)) do |machine, object, *args|
1996: machine.events.valid_for(object, *args).map {|event| event.name}
1997: end
1998:
1999: # Gets the next possible transitions that can be run on the current
2000: # object
2001: define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
2002: machine.events.transitions_for(object, *args)
2003: end
2004:
2005: # Fire an arbitrary event for this machine
2006: define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
2007: machine.events.fetch(event).fire(object, *args)
2008: end
2009:
2010: # Add helpers for tracking the event / transition to invoke when the
2011: # action is called
2012: if action
2013: event_attribute = attribute(:event)
2014: define_helper(:instance, event_attribute) do |machine, object|
2015: # Interpret non-blank events as present
2016: event = machine.read(object, :event, true)
2017: event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
2018: end
2019:
2020: # A roundabout way of writing the attribute is used here so that
2021: # integrations can hook into this modification
2022: define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
2023: machine.write(object, :event, value, true)
2024: end
2025:
2026: event_transition_attribute = attribute(:event_transition)
2027: define_helper :instance, "protected; attr_accessor \#{event_transition_attribute.inspect}\n", __FILE__, __LINE__ + 1
2028: end
2029: end
Adds helper methods for interacting with the state machine, including for states, events, and transitions
# File lib/state_machine/machine.rb, line 1950
1950: def define_helpers
1951: define_state_accessor
1952: define_state_predicate
1953: define_event_helpers
1954: define_path_helpers
1955: define_action_helpers if define_action_helpers?
1956: define_name_helpers
1957: end
Adds helper methods for accessing naming information about states and events on the owner class
# File lib/state_machine/machine.rb, line 2119
2119: def define_name_helpers
2120: # Gets the humanized version of a state
2121: define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2122: machine.states.fetch(state).human_name(klass)
2123: end
2124:
2125: # Gets the humanized version of an event
2126: define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2127: machine.events.fetch(event).human_name(klass)
2128: end
2129:
2130: # Gets the state name for the current value
2131: define_helper(:instance, attribute(:name)) do |machine, object|
2132: machine.states.match!(object).name
2133: end
2134:
2135: # Gets the human state name for the current value
2136: define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2137: machine.states.match!(object).human_name(object.class)
2138: end
2139: end
Adds helper methods for getting information about this state machine‘s available transition paths
# File lib/state_machine/machine.rb, line 2036
2036: def define_path_helpers
2037: # Gets the paths of transitions available to the current object
2038: define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2039: machine.paths_for(object, *args)
2040: end
2041: end
Defines the with/without scope helpers for this attribute. Both the singular and plural versions of the attribute are defined for each scope helper. A custom plural can be specified if it cannot be automatically determined by either calling pluralize on the attribute name or adding an "s" to the end of the name.
# File lib/state_machine/machine.rb, line 2146
2146: def define_scopes(custom_plural = nil)
2147: plural = custom_plural || pluralize(name)
2148:
2149: [name, plural].uniq.each do |name|
2150: [:with, :without].each do |kind|
2151: method = "#{kind}_#{name}"
2152:
2153: if scope = send("create_#{kind}_scope", method)
2154: # Converts state names to their corresponding values so that they
2155: # can be looked up properly
2156: define_helper(:class, method) do |machine, klass, *states|
2157: run_scope(scope, machine, klass, states)
2158: end
2159: end
2160: end
2161: end
2162: end
Adds reader/writer methods for accessing the state attribute
# File lib/state_machine/machine.rb, line 1972
1972: def define_state_accessor
1973: attribute = self.attribute
1974:
1975: @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
1976: @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
1977: end
Defines the initial values for state machine attributes. Static values are set prior to the original initialize method and dynamic values are set after the initialize method in case it is dependent on it.
# File lib/state_machine/machine.rb, line 1962
1962: def define_state_initializer
1963: define_helper :instance, "def initialize(*)\nself.class.state_machines.initialize_states(self) { super }\nend\n", __FILE__, __LINE__ + 1
1964: end
Adds predicate method to the owner class for determining the name of the current state
# File lib/state_machine/machine.rb, line 1981
1981: def define_state_predicate
1982: call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
1983: define_helper :instance, "def \#{name}?(*args)\nargs.empty? && (\#{call_super} || defined?(super)) ? super : self.class.state_machine(\#{name.inspect}).states.matches?(self, *args)\nend\n", __FILE__, __LINE__ + 1
1984: end
Determines if the machine‘s attribute needs to be initialized. This will only be true if the machine‘s attribute is blank.
# File lib/state_machine/machine.rb, line 1943
1943: def initialize_state?(object)
1944: value = read(object, :state)
1945: (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1946: end
Determines whether there‘s already a helper method defined within the given scope. This is true only if one of the owner‘s ancestors defines the method and is further along in the ancestor chain than this machine‘s helper module.
# File lib/state_machine/machine.rb, line 2092
2092: def owner_class_ancestor_has_method?(scope, method)
2093: superclasses = owner_class.ancestors[1..-1].select {|ancestor| ancestor.is_a?(Class)}
2094:
2095: if scope == :class
2096: # Use singleton classes
2097: current = (class << owner_class; self; end)
2098: superclass = superclasses.first
2099: else
2100: current = owner_class
2101: superclass = owner_class.superclass
2102: end
2103:
2104: # Generate the list of modules that *only* occur in the owner class, but
2105: # were included *prior* to the helper modules, in addition to the
2106: # superclasses
2107: ancestors = current.ancestors - superclass.ancestors + superclasses
2108: ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2109:
2110: # Search for for the first ancestor that defined this method
2111: ancestors.detect do |ancestor|
2112: ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(Class)
2113: ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2114: end
2115: end
Pluralizes the given word using pluralize (if available) or simply adding an "s" to the end of the word
# File lib/state_machine/machine.rb, line 2173
2173: def pluralize(word)
2174: word = word.to_s
2175: if word.respond_to?(:pluralize)
2176: word.pluralize
2177: else
2178: "#{name}s"
2179: end
2180: end
Generates the results for the given scope based on one or more states to filter by
# File lib/state_machine/machine.rb, line 2166
2166: def run_scope(scope, machine, klass, states)
2167: values = states.flatten.map {|state| machine.states.fetch(state).value}
2168: scope.call(klass, values)
2169: end
Looks up other machines that have been defined in the owner class and are targeting the same attribute as this machine. When accessing sibling machines, they will be automatically copied for the current class if they haven‘t been already. This ensures that any configuration changes made to the sibling machines only affect this class and not any base class that may have originally defined the machine.
# File lib/state_machine/machine.rb, line 1932
1932: def sibling_machines
1933: owner_class.state_machines.inject([]) do |machines, (name, machine)|
1934: if machine.attribute == attribute && machine != self
1935: machines << (owner_class.state_machine(name) {})
1936: end
1937: machines
1938: end
1939: end