lib/state_machine/machine.rb in state_machine-0.4.0 vs lib/state_machine/machine.rb in state_machine-0.4.1
- old
+ new
@@ -15,15 +15,15 @@
#
# A state machine may not necessarily know all of the possible states for
# an object since they can be any arbitrary value. As a result, anything
# that relies on a list of all possible states should keep in mind that if
# a state has not been referenced *anywhere* in the state machine definition,
- # then it will *not* be a known state unless the +other_states+ is used.
+ # then it will *not* be a known state unless the +other_states+ helper is used.
#
# == State values
#
- # While string are the most common object type used for setting values on
+ # While strings are the most common object type used for setting values on
# the state of the machine, there are no restrictions on what can be used.
# This means that symbols, integers, dates/times, etc. can all be used.
#
# With string states:
#
@@ -47,10 +47,12 @@
#
# With time states:
#
# class Switch
# state_machine :activated_at
+ # before_transition :to => nil, :do => lambda {...}
+ #
# event :activate do
# transition :to => lambda {Time.now}
# end
#
# event :deactivate do
@@ -217,20 +219,23 @@
# StateMachine::Machine.find_or_create(Switch, 'status', :initial => 'off')
#
# 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)
+ def find_or_create(owner_class, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
- attribute = args.any? ? args.first.to_s : 'state'
+ attribute = (args.first || 'state').to_s
# Attempts to find an existing machine
if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
machine = machine.within_context(owner_class, options) unless machine.owner_class == owner_class
+
+ # Evaluate caller block for DSL
+ machine.instance_eval(&block) if block_given?
else
# No existing machine: create a new one
- machine = new(owner_class, attribute, options)
+ machine = new(owner_class, attribute, options, &block)
end
machine
end
@@ -281,20 +286,23 @@
owner_class.class_eval do
extend StateMachine::ClassMethods
include StateMachine::InstanceMethods
end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
- # Initialize the context of the machine
+ # Initialize the class context of the machine
set_context(owner_class, :initial => options[:initial], :integration => options[:integration], &block)
# Set integration-specific configurations
@action ||= default_action unless options.include?(:action)
define_attribute_accessor
define_scopes(options[:plural])
# Call after hook for integration-specific extensions
after_initialize
+
+ # Evaluate caller block for DSL
+ instance_eval(&block) if block_given?
end
# Creates a copy of this machine in addition to copies of each associated
# event, so that the list of transitions for each event don't conflict
# with different machines
@@ -311,11 +319,11 @@
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
end
# Creates a copy of this machine within the context of the given class.
# This should be used for inheritance support of state machines.
- def within_context(owner_class, options = {}) #:nodoc:
+ def within_context(owner_class, options = {}, &block) #:nodoc:
machine = dup
machine.set_context(owner_class, {:integration => @integration}.merge(options))
machine
end
@@ -330,14 +338,12 @@
# creation.
def set_context(owner_class, options = {}) #:nodoc:
assert_valid_keys(options, :initial, :integration)
@owner_class = owner_class
- if options[:initial]
- @initial_state = options[:initial]
- add_states([@initial_state]) unless @initial_state.is_a?(Proc)
- end
+ @initial_state = options[:initial] if options[:initial]
+ add_states([@initial_state])
# Find an integration that can be used for implementing various parts
# of the state machine that may behave differently in different libraries
if @integration = options[:integration] || StateMachine::Integrations.constants.find {|name| StateMachine::Integrations.const_get(name).matches?(owner_class)}
extend StateMachine::Integrations.const_get(@integration.to_s.gsub(/(?:^|_)(.)/) {$1.upcase})
@@ -349,42 +355,53 @@
owner_class.state_machines[attribute] = self
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 proc to help determine the actual value of the initial
- # state.
+ # passed into the lambda block to help determine the actual value of the
+ # initial state.
#
# == Examples
#
- # With static initial state:
+ # With a static initial state:
#
# class Vehicle
# state_machine :initial => 'parked' do
# ...
# end
# end
#
+ # vehicle = Vehicle.new
# Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
#
- # With dynamic initial state:
+ # 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) # => "idling"
+ #
+ # vehicle.force_idle = false
+ # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
def initial_state(object)
@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state
end
# Defines additional states that are possible in the state machine, but
# which are derived outside of any events/transitions or possibly
- # dynamically via Proc. This allows the creation of state conditionals
- # which are not defined in the standard :to or :from structure.
+ # dynamically via a lambda block. This allows the given states to be:
+ # * Queried via instance-level predicates
+ # * Included in GraphViz visualizations
+ # * Used in :except_from and :except_to transition/callback conditionals
#
# == Example
#
# class Vehicle
# state_machine :initial => 'parked' do
@@ -448,11 +465,11 @@
# %w(parked idling stalled)
# end
#
# state_machine do
# event :park do
- # transition :to => 'parked', :from => Car.safe_states
+ # transition :to => 'parked', :from => Vehicle.safe_states
# end
# end
# end
#
# == Example
@@ -683,27 +700,51 @@
require 'rubygems'
require 'graphviz'
graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
+ # Tracks unique identifiers for dynamic states (via lambda blocks)
+ dynamic_states = {}
+
# Add nodes
states.each do |state|
shape = state == @initial_state ? 'doublecircle' : 'circle'
- state = state.is_a?(Proc) ? 'lambda' : state.to_s
- graph.add_node(state, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
+
+ # Use GraphViz-friendly name/label for dynamic/nil states
+ if state.is_a?(Proc)
+ name = "lambda#{dynamic_states.keys.length}"
+ label = '*'
+ dynamic_states[state] = name
+ else
+ name = label = state.nil? ? 'nil' : state.to_s
+ end
+
+ graph.add_node(name, :label => label, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
end
# Add edges
events.values.each do |event|
event.guards.each do |guard|
# From states: :from, everything but :except states, or all states
- from_states = Array(guard.requirements[:from]) || guard.requirements[:except_from] && (states - Array(guard.requirements[:except_from])) || states
- to_state = guard.requirements[:to]
- to_state = to_state.is_a?(Proc) ? 'lambda' : to_state.to_s if to_state
+ from_states = guard.requirements[:from] || guard.requirements[:except_from] && (states - guard.requirements[:except_from]) || states
+ if to_state = guard.requirements[:to]
+ to_state = to_state.first
+
+ # Convert to GraphViz-friendly name
+ to_state = case to_state
+ when Proc; dynamic_states[to_state]
+ when nil; 'nil'
+ else; to_state.to_s; end
+ end
from_states.each do |from_state|
- from_state = from_state.to_s
+ # Convert to GraphViz-friendly name
+ from_state = case from_state
+ when Proc; dynamic_states[from_state]
+ when nil; 'nil'
+ else; from_state.to_s; end
+
graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
end
end
end
@@ -726,12 +767,12 @@
# transition on the attribute for this machine. This may change
# depending on the configured integration for the owner class.
def default_action
end
- # Adds reader/writer methods for accessing the attribute that this state
- # machine is defined for.
+ # Adds reader/writer/prediate methods for accessing the attribute that
+ # this state machine is defined for.
def define_attribute_accessor
attribute = self.attribute
owner_class.class_eval do
attr_reader attribute unless method_defined?(attribute) || private_method_defined?(attribute)
@@ -788,10 +829,10 @@
@states += new_states
# Add state predicates
attribute = self.attribute
new_states.each do |state|
- if state.is_a?(String) || state.is_a?(Symbol)
+ if state && (state.is_a?(String) || state.is_a?(Symbol))
name = "#{state}?"
owner_class.class_eval do
# Checks whether the current state is equal to the given value
define_method(name) do