require 'state_machine/extensions'
require 'state_machine/assertions'
require 'state_machine/integrations'
require 'state_machine/state'
require 'state_machine/event'
require 'state_machine/callback'
require 'state_machine/node_collection'
require 'state_machine/state_collection'
require 'state_machine/matcher_helpers'
module StateMachine
# 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.
#
# == 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 StateMachine::Machine#before_transition
# and StateMachine::Machine#after_transition for documentation
# on how to define new callbacks.
#
# === 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.
#
# *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
#
# 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, result)
# logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
# 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)
# 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 |machine|
# machine.before_transition klass.method(:before_transition)
# end
# end
#
# Additional observer-like behavior may be exposed by the various integrations
# available. See below for more information.
#
# == 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
# transition :idling => :parked
# end
# end
#
# def park(kind = :parallel, *args)
# take_deep_breath if kind == :parallel
# super(*args)
# end
#
# def take_deep_breath
# sleep 3
# end
# end
#
# In the above example, the +park+ instance method that's generated on the
# Vehicle class (by the associated event) is overriden with custom behavior
# that takes an additional argument. Once this behavior is complete, the
# original method from the state machine is invoked by simply calling
# super(*args).
#
# The same technique can be used for +state+, +state_name+, and all other
# instance *and* class methods on the Vehicle class.
#
# == 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,
# ActiveRecord, DataMapper, 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.
class Machine
include Assertions
include MatcherHelpers
class << self
# The default message to use when invalidating objects that fail to
# transition when triggering an event
attr_accessor :default_invalid_message
# 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).
def find_or_create(owner_class, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
attribute = args.first || :state
# Attempts to find an existing machine
if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
# Create a copy of the state machine if it's being created by a subclass
unless machine.owner_class == owner_class
machine = machine.clone
machine.initial_state = options[:initial] if options.include?(:initial)
machine.owner_class = owner_class
end
# Evaluate DSL caller block
machine.instance_eval(&block) if block_given?
else
# No existing machine: create a new one
machine = new(owner_class, attribute, options, &block)
end
machine
end
# 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 - A comma-delimited string of files to load that
# contain the state machine definitions to draw
# * :path - The path to write the graph file to
# * :format - The image format to generate the graph in
# * :font - The name of the font to draw state names in
def draw(class_names, options = {})
raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
# Load any files
if files = options.delete(:file)
files.split(',').each {|file| require file}
end
class_names.split(',').each do |class_name|
# Navigate through the namespace structure to get to the class
klass = Object
class_name.split('::').each do |name|
klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
end
# Draw each of the class's state machines
klass.state_machines.each do |name, machine|
machine.draw(options)
end
end
end
end
# Set defaults
self.default_invalid_message = 'cannot be transitioned via :%s from :%s'
# The class that the machine is defined in
attr_accessor :owner_class
# The attribute for which the machine is being defined
attr_reader :attribute
# The events that trigger transitions. These are sorted, by default, in the
# order in which they were defined.
attr_reader :events
# 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.
attr_reader :states
# The callbacks to invoke before/after a transition is performed
#
# Maps :before => callbacks and :after => callbacks
attr_reader :callbacks
# The action to invoke when an object transitions
attr_reader :action
# 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.
attr_reader :namespace
# Creates a new state machine for the given attribute
def initialize(owner_class, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration, :invalid_message)
# Set machine configuration
@attribute = args.first || :state
@events = NodeCollection.new
@states = StateCollection.new
@callbacks = {:before => [], :after => []}
@namespace = options[:namespace]
@invalid_message = options[:invalid_message]
self.owner_class = owner_class
self.initial_state = options[:initial]
# Find an integration that matches this machine's owner class
if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
extend integration
end
# Set integration-specific configurations
@action = options.include?(:action) ? options[:action] : default_action
define_attribute_helpers
define_scopes(options[:plural])
# Call after hook for integration-specific extensions
after_initialize
# Evaluate DSL caller block
instance_eval(&block) if block_given?
end
# 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.
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}
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.
def owner_class=(klass)
@owner_class = klass
# Add class-/instance-level methods to the owner class for state initialization
owner_class.class_eval do
extend StateMachine::ClassMethods
include StateMachine::InstanceMethods
end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
# Create modules for extending the class with state/event-specific methods
class_helper_module = @class_helper_module = Module.new
instance_helper_module = @instance_helper_module = Module.new
owner_class.class_eval do
extend class_helper_module
include instance_helper_module
end
# Record this machine as matched to the attribute in the current owner
# class. This will override any machines mapped to the same attribute
# in any superclasses.
owner_class.state_machines[attribute] = self
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.
def initial_state=(new_initial_state)
@initial_state = new_initial_state
add_states([@initial_state]) unless @initial_state.is_a?(Proc)
# Update all states to reflect the new initial state
states.each {|state| state.initial = (state.name == @initial_state)}
end
# Defines a new instance method with the given name on the machine's owner
# class. If the method is already defined in the class, then this will not
# override it.
#
# Not that in order for inheritance to work properly within state machines,
# any states/events/etc. must be referred to from the current state machine
# associated with the executing class.
#
# Example:
#
# attribute = machine.attribute
# machine.define_instance_method(:parked?) do |machine, object|
# machine.state?(object, :parked)
# end
def define_instance_method(method, &block)
attribute = self.attribute
@instance_helper_module.class_eval do
define_method(method) do |*args|
block.call(self.class.state_machines[attribute], self, *args)
end
end
end
attr_reader :instance_helper_module
# Defines a new class method with the given name on the machine's owner
# class. If the method is already defined in the class, then this will not
# override it.
#
# Not that in order for inheritance to work properly within state machines,
# any states/events/etc. must be referred to from the current state machine
# associated with the executing class.
#
# Example:
#
# machine.define_class_method(:states) do |machine, klass|
# machine.states.keys
# end
def define_class_method(method, &block)
attribute = self.attribute
@class_helper_module.class_eval do
define_method(method) do |*args|
block.call(self.state_machines[attribute], self, *args)
end
end
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.
#
# == Examples
#
# With a static initial state:
#
# class Vehicle
# state_machine :initial => :parked do
# ...
# end
# end
#
# vehicle = Vehicle.new
# Vehicle.state_machines[:state].initial_state(vehicle) # => #
#
# 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_machines[:state].initial_state(vehicle) # => #
#
# vehicle.force_idle = false
# Vehicle.state_machines[:state].initial_state(vehicle) # => #
def initial_state(object)
states.fetch(@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state)
end
# 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).
# * :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.
#
# == 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 states (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 :state_id, :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
#
# states.each {|state| self.state(state.name, :value => VehicleState.find_by_name(state.name.to_s).id)}
# end
# end
#
# In the above example, each known state is configured to store it's
# associated database id in the +state_id+ attribute.
#
# === 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"
# vehicle.state # => "parked"
#
# 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 # in state "backing_up"
#
# == 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.
def state(*names, &block)
options = names.last.is_a?(Hash) ? names.pop : {}
assert_valid_keys(options, :value, :if)
states = add_states(names)
states.each do |state|
if options.include?(:value)
state.value = options[:value]
self.states.update(state)
end
state.matcher = options[:if] if options.include?(:if)
state.context(&block) if block_given?
end
states.length == 1 ? states.first : states
end
alias_method :other_states, :state
# Determines whether the given object is in a specific state. If the
# object's current value doesn't match the state, then this will return
# false, otherwise true. If the given state is unknown, then an ArgumentError
# will be raised.
#
# == Examples
#
# class Vehicle
# state_machine :initial => :parked do
# other_states :idling
# end
# end
#
# machine = Vehicle.state_machines[:state]
# vehicle = Vehicle.new # => #
#
# machine.state?(vehicle, :parked) # => true
# machine.state?(vehicle, :idling) # => false
# machine.state?(vehicle, :invalid) # => ArgumentError: :invalid is an invalid key for :name index
def state?(object, name)
states.fetch(name).matches?(object.send(attribute))
end
# Determines the current state of the given object as configured by this
# state machine. This will attempt to find a known state that matches
# the value of the attribute on the object. If no state is found, then
# an ArgumentError will be raised.
#
# == Examples
#
# class Vehicle
# state_machine :initial => :parked do
# other_states :idling
# end
# end
#
# machine = Vehicle.state_machines[:state]
#
# vehicle = Vehicle.new # => #
# machine.state_for(vehicle) # => #
#
# vehicle.state = 'idling'
# machine.state_for(vehicle) # => #
#
# vehicle.state = 'invalid'
# machine.state_for(vehicle) # => ArgumentError: "invalid" is not a known state value
def state_for(object)
value = object.send(attribute)
state = states[value, :value] || states.detect {|state| state.matches?(value)}
raise ArgumentError, "#{value.inspect} is not a known #{attribute} value" unless state
state
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.
#
# == Instance methods
#
# The following instance methods are generated when a new event is defined
# (the "park" event is used as an example):
# * can_park? - Checks whether the "park" event can be fired given
# the current state of the object.
# * next_park_transition - 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.
# * park(run_action = true) - Fires the "park" event, transitioning
# from the current state to the next valid state.
# * park!(run_action = true) - Fires the "park" event, transitioning
# from the current state to the next valid state. If the transition fails,
# then a StateMachine::InvalidTransition error will be raised.
#
# With a namespace of "car", the above names map to the following methods:
# * can_park_car?
# * next_park_car_transition
# * park_car
# * park_car!
#
# == 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?
# 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
#
# == 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
# end
# end
# end
def event(*names, &block)
events = names.collect do |name|
unless event = self.events[name]
self.events << event = Event.new(self, name)
end
if block_given?
event.instance_eval(&block)
add_states(event.known_states)
end
event
end
events.length == 1 ? events.first : events
end
alias_method :on, :event
# 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 the only argument, in the :do option,
# or as a block. For example,
#
# class Vehicle
# state_machine do
# before_transition :set_alarm
# before_transition all => :parked :do => :set_alarm
# before_transition all => :parked do |vehicle, transition|
# vehicle.set_alarm
# end
# ...
# end
# end
#
# == 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 for +all+ (matches every state/event in the machine)
# * same - Matches the same state being transitioned from
#
# 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
#
# == 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?
# before_transition :on => :ignite, :unless => :seatbelt_on?
#
# === 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 :to => :parked, :do => lambda {|vehicle| vehicle.set_alarm}
#
# # Specifies 2 parameters (object being transitioned and actual transition)
# before_transition :to => :parked, :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
# 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.
#
# == 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 :to => :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.
def before_transition(options = {}, &block)
add_callback(:before, options.is_a?(Hash) ? options : {:do => options}, &block)
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.
def after_transition(options = {}, &block)
add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
end
# Marks the given object as invalid after failing to transition via the
# given event.
#
# By default, this is a no-op.
def invalidate(object, event)
end
# Resets an errors previously added when invalidating the given object
#
# By default, this is a no-op.
def reset(object)
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.
def within_transaction(object)
yield
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:
# * :name - The name of the file to write to (without the file extension).
# Default is "#{owner_class.name}_#{attribute}"
# * :path - The path to write the graph file to. Default is the
# current directory (".").
# * :format - The image format to generate the graph in.
# Default is "png'.
# * :font - The name of the font to draw state names in.
# Default is "Arial".
# * :orientation - The direction of the graph ("portrait" or
# "landscape"). Default is "portrait".
# * :output - Whether to generate the output of the graph
def draw(options = {})
options = {
:name => "#{owner_class.name}_#{attribute}",
:path => '.',
:format => 'png',
:font => 'Arial',
:orientation => 'portrait',
:output => true
}.merge(options)
assert_valid_keys(options, :name, :path, :format, :font, :orientation, :output)
begin
# Load the graphviz library
require 'rubygems'
require 'graphviz'
graph = GraphViz.new('G',
:output => options[:format],
:file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"),
:rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB'
)
# Add nodes
states.by_priority.each do |state|
node = state.draw(graph)
node.fontname = options[:font]
end
# Add edges
events.each do |event|
edges = event.draw(graph)
edges.each {|edge| edge.fontname = options[:font]}
end
# Generate the graph
graph.output if options[:output]
graph
rescue LoadError
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
false
end
end
protected
# Runs additional initialization hooks. By default, this is a no-op.
def after_initialize
end
# Gets the default action that should be invoked when performing a
# transition on the attribute for this machine. This may change
# depending on the configured integration for the owner class.
def default_action
end
# Adds helper methods for interacting with this state machine's attribute,
# including reader, writer, and predicate methods
def define_attribute_helpers
define_attribute_accessor
define_attribute_predicate
attribute = self.attribute
# Gets the state name for the current value
define_instance_method("#{attribute}_name") do |machine, object|
machine.state_for(object).name
end
end
# Adds reader/writer methods for accessing the attribute
def define_attribute_accessor
attribute = self.attribute
@instance_helper_module.class_eval do
attr_reader attribute
attr_writer attribute
end
end
# Adds predicate method to the owner class for determining the name of the
# current state
def define_attribute_predicate
attribute = self.attribute
# Checks whether the current state is a given value
define_instance_method("#{attribute}?") do |machine, object, state|
machine.state?(object, state)
end
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.
def define_scopes(custom_plural = nil)
attribute = self.attribute
plural = custom_plural || (attribute.to_s.respond_to?(:pluralize) ? attribute.to_s.pluralize : "#{attribute}s")
[attribute, plural].uniq.each do |name|
[:with, :without].each do |kind|
method = "#{kind}_#{name}"
if scope = send("create_#{kind}_scope", method)
# Converts state names to their corresponding values so that they
# can be looked up properly
define_class_method(method) do |machine, klass, *states|
machine_states = machine.states
values = states.flatten.map {|state| machine_states.fetch(state).value}
# Invoke the original scope implementation
scope.call(klass, values)
end
end
end
end
end
# Creates a scope for finding objects *with* a particular value or values
# for the attribute.
#
# This is only applicable to specific integrations.
def create_with_scope(name)
end
# Creates a scope for finding objects *without* a particular value or
# values for the attribute.
#
# This is only applicable to specific integrations.
def create_without_scope(name)
end
# Adds a new transition callback of the given type.
def add_callback(type, options, &block)
callbacks[type] << callback = Callback.new(options, &block)
add_states(callback.known_states)
callback
end
# Tracks the given set of states in the list of all known states for
# this machine
def add_states(new_states)
new_states.collect do |new_state|
unless state = states[new_state]
states << state = State.new(self, new_state)
end
state
end
end
# Generates the message to use when invalidating the given object after
# failing to transition on a specific event
def invalid_message(object, event)
(@invalid_message || self.class.default_invalid_message) % [event.name, state_for(object).name]
end
end
end