lib/state_machine/machine.rb in state_machine-0.7.5 vs lib/state_machine/machine.rb in state_machine-0.7.6
- old
+ new
@@ -111,18 +111,18 @@
#
# == 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.
+ # 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.
+ # *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
@@ -227,11 +227,11 @@
# end
# end
#
# [Vehicle, Switch, Project].each do |klass|
# klass.state_machines.each do |attribute, machine|
- # machine.before_transition klass.method(:before_transition)
+ # 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.
@@ -298,14 +298,14 @@
# 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
+ name = args.first || :state
# Find an existing machine
- if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
# Only create a new copy if changes are being made to the machine in
# a subclass
if machine.owner_class != owner_class && (options.any? || block_given?)
machine = machine.clone
machine.initial_state = options[:initial] if options.include?(:initial)
@@ -314,11 +314,11 @@
# Evaluate DSL
machine.instance_eval(&block) if block_given?
else
# No existing machine: create a new one
- machine = new(owner_class, attribute, options, &block)
+ machine = new(owner_class, name, options, &block)
end
machine
end
@@ -345,11 +345,11 @@
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|
+ klass.state_machines.each_value do |machine|
machine.draw(options)
end
end
end
end
@@ -363,12 +363,13 @@
}
# 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 name of the machine, used for scoping methods generated for the
+ # machine as a whole (not states or events)
+ attr_reader :name
# The events that trigger transitions. These are sorted, by default, in
# the order in which they were defined.
attr_reader :events
@@ -400,11 +401,11 @@
attr_reader :use_transactions
# 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, :messages, :use_transactions)
+ assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
# 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
options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
@@ -412,11 +413,12 @@
# Add machine-wide defaults
options = {:use_transactions => true}.merge(options)
# Set machine configuration
- @attribute = args.first || :state
+ @name = args.first || :state
+ @attribute = options[:attribute] || @name
@events = EventCollection.new(self)
@states = StateCollection.new(self)
@callbacks = {:before => [], :after => []}
@namespace = options[:namespace]
@messages = options[:messages] || {}
@@ -465,14 +467,13 @@
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
+ # Record this machine as matched to the name in the current owner class.
+ # This will override any machines mapped to the same name in any superclasses.
+ owner_class.state_machines[name] = 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.
@@ -482,26 +483,31 @@
# Update all states to reflect the new initial state
states.each {|state| state.initial = (state.name == @initial_state)}
end
+ # Gets the actual name of the attribute on the machine's owner class that
+ # stores data with the given name.
+ def attribute(name = :state)
+ name == :state ? @attribute : :"#{self.name}_#{name}"
+ 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.
#
# Example:
#
- # attribute = machine.attribute
# machine.define_instance_method(:state_name) do |machine, object|
# machine.states.match(object)
# end
def define_instance_method(method, &block)
- attribute = self.attribute
+ name = self.name
@instance_helper_module.class_eval do
define_method(method) do |*args|
- block.call(self.class.state_machine(attribute), self, *args)
+ block.call(self.class.state_machine(name), self, *args)
end
end
end
attr_reader :instance_helper_module
@@ -513,15 +519,15 @@
#
# machine.define_class_method(:states) do |machine, klass|
# machine.states.keys
# end
def define_class_method(method, &block)
- attribute = self.attribute
+ name = self.name
@class_helper_module.class_eval do
define_method(method) do |*args|
- block.call(self.state_machine(attribute), self, *args)
+ block.call(self.state_machine(name), self, *args)
end
end
end
# Gets the initial state of the machine for the given object. If a dynamic
@@ -614,11 +620,11 @@
#
# class VehicleState < ActiveRecord::Base
# end
#
# class Vehicle < ActiveRecord::Base
- # state_machine :state_id, :initial => :parked do
+ # state_machine :attribute => :state_id, :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
#
# states.each do |state|
@@ -822,41 +828,45 @@
states.length == 1 ? states.first : states
end
alias_method :other_states, :state
- # Gets the current value stored in the given object's state.
+ # 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) # => "parked"
- def read(object)
- object.send(attribute)
+ # 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
+ def read(object, attribute, ivar = false)
+ attribute = self.attribute(attribute)
+ ivar ? object.instance_variable_get("@#{attribute}") : object.send(attribute)
end
- # Sets a new value in the given object's state.
+ # 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, 'idling')
- # vehicle.state # => "idling"
- def write(object, value)
- object.send("#{attribute}=", value)
+ # 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"
+ def write(object, attribute, value)
+ object.send("#{self.attribute(attribute)}=", value)
end
# Defines one or more events for the machine and the transitions that can
# be performed when those events are run.
#
@@ -1110,16 +1120,16 @@
#
# For example,
#
# class Vehicle
# # Only specifies one parameter (the object being transitioned)
- # before_transition :to => :parked do |vehicle|
+ # before_transition all => :parked do |vehicle|
# vehicle.set_alarm
# end
#
# # Specifies 2 parameters (object being transitioned and actual transition)
- # before_transition :to => :parked do |vehicle, 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
@@ -1141,11 +1151,11 @@
#
# # 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?
+ # 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
@@ -1203,11 +1213,11 @@
# This requires both the Ruby graphviz gem and the graphviz library be
# installed on the system.
#
# Configuration options:
# * <tt>:name</tt> - The name of the file to write to (without the file extension).
- # Default is "#{owner_class.name}_#{attribute}"
+ # Default is "#{owner_class.name}_#{name}"
# * <tt>:path</tt> - The path to write the graph file to. Default is the
# current directory (".").
# * <tt>:format</tt> - The image format to generate the graph in.
# Default is "png'.
# * <tt>:font</tt> - The name of the font to draw state names in.
@@ -1215,11 +1225,11 @@
# * <tt>:orientation</tt> - The direction of the graph ("portrait" or
# "landscape"). Default is "portrait".
# * <tt>:output</tt> - Whether to generate the output of the graph
def draw(options = {})
options = {
- :name => "#{owner_class.name}_#{attribute}",
+ :name => "#{owner_class.name}_#{name}",
:path => '.',
:format => 'png',
:font => 'Arial',
:orientation => 'portrait',
:output => true
@@ -1270,11 +1280,11 @@
define_state_predicate
define_event_helpers
define_action_helpers if action
# Gets the state name for the current value
- define_instance_method("#{attribute}_name") do |machine, object|
+ define_instance_method(attribute(:name)) do |machine, object|
machine.states.match!(object).name
end
end
# Adds reader/writer methods for accessing the state attribute
@@ -1287,44 +1297,44 @@
end
# Adds predicate method to the owner class for determining the name of the
# current state
def define_state_predicate
- define_instance_method("#{attribute}?") do |machine, object, state|
+ define_instance_method("#{name}?") do |machine, object, state|
machine.states.matches?(object, state)
end
end
# Adds helper methods for getting information about this state machine's
# events
def define_event_helpers
# Gets the events that are allowed to fire on the current object
- define_instance_method("#{attribute}_events") do |machine, object|
+ define_instance_method(attribute(:events)) do |machine, object|
machine.events.valid_for(object).map {|event| event.name}
end
# Gets the next possible transitions that can be run on the current
# object
- define_instance_method("#{attribute}_transitions") do |machine, object, *args|
+ define_instance_method(attribute(:transitions)) do |machine, object, *args|
machine.events.transitions_for(object, *args)
end
# Add helpers for interacting with the action
if action
- attribute = self.attribute
-
# Tracks the event / transition to invoke when the action is called
+ event_attribute = attribute(:event)
+ event_transition_attribute = attribute(:event_transition)
@instance_helper_module.class_eval do
- attr_writer "#{attribute}_event"
+ attr_writer event_attribute
protected
- attr_accessor "#{attribute}_event_transition"
+ attr_accessor event_transition_attribute
end
# Interpret non-blank events as present
- define_instance_method("#{attribute}_event") do |machine, object|
- event = object.instance_variable_get("@#{attribute}_event")
+ define_instance_method(attribute(:event)) do |machine, object|
+ event = machine.read(object, :event, true)
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
end
end
end
@@ -1332,11 +1342,11 @@
# is invoked
def define_action_helpers(action_hook = self.action)
action = self.action
private_method = owner_class.private_method_defined?(action_hook)
- if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|attribute, machine| machine.action == action && machine != self}
+ if (owner_class.method_defined?(action_hook) || private_method) && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
# Action is defined and hasn't already been overridden by another machine
@instance_helper_module.class_eval do
# Override the default action to invoke the before / after hooks
define_method(action_hook) do |*args|
self.class.state_machines.fire_event_attributes(self, action) { super(*args) }
@@ -1356,12 +1366,12 @@
# 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)
- plural = custom_plural || (attribute.to_s.respond_to?(:pluralize) ? attribute.to_s.pluralize : "#{attribute}s")
+ plural = custom_plural || (name.to_s.respond_to?(:pluralize) ? name.to_s.pluralize : "#{name}s")
- [attribute, plural].uniq.each do |name|
+ [name, 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