lib/state_machine/transition.rb in state_machine-0.8.1 vs lib/state_machine/transition.rb in state_machine-0.9.0
- old
+ new
@@ -1,5 +1,7 @@
+require 'state_machine/transition_collection'
+
module StateMachine
# An invalid transition was attempted
class InvalidTransition < StandardError
end
@@ -8,87 +10,10 @@
# Transitions consist of:
# * An event
# * A starting state
# * An ending state
class Transition
- class << self
- # Runs one or more transitions in parallel. All transitions will run
- # through the following steps:
- # 1. Before callbacks
- # 2. Persist state
- # 3. Invoke action
- # 4. After callbacks (if configured)
- # 5. Rollback (if action is unsuccessful)
- #
- # Configuration options:
- # * <tt>:action</tt> - Whether to run the action configured for each transition
- # * <tt>:after</tt> - Whether to run after callbacks
- #
- # If a block is passed to this method, that block will be called instead
- # of invoking each transition's action.
- def perform(transitions, options = {})
- # Validate that the transitions are for separate machines / attributes
- attributes = transitions.map {|transition| transition.attribute}.uniq
- raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != transitions.length
-
- success = false
-
- # Run before callbacks. If any callback halts, then the entire chain
- # is halted for every transition.
- if transitions.all? {|transition| transition.before}
- # Persist the new state for each attribute
- transitions.each {|transition| transition.persist}
-
- # Run the actions associated with each machine
- begin
- results = {}
- success =
- if block_given?
- # Block was given: use the result for each transition
- result = yield
- transitions.each {|transition| results[transition.action] = result}
- !!result
- elsif options[:action] == false
- # Skip the action
- true
- else
- # Run each transition's action (only once)
- object = transitions.first.object
- transitions.all? do |transition|
- action = transition.action
- action && !results.include?(action) ? results[action] = object.send(action) : true
- end
- end
- rescue Exception
- # Action failed: rollback
- transitions.each {|transition| transition.rollback}
- raise
- end
-
- # Run after callbacks even when the actions failed. The :after option
- # is ignored if the transitions were unsuccessful.
- transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
-
- # Rollback the transitions if the transaction was unsuccessful
- transitions.each {|transition| transition.rollback} unless success
- end
-
- success
- end
-
- # Runs one or more transitions within a transaction. See StateMachine::Transition.perform
- # for more information.
- def perform_within_transaction(transitions, options = {})
- success = false
- transitions.first.within_transaction do
- success = perform(transitions, options)
- end
-
- success
- end
- end
-
# The object being transitioned
attr_reader :object
# The state machine for which this transition is defined
attr_reader :machine
@@ -122,15 +47,19 @@
attr_accessor :args
# The result of invoking the action associated with the machine
attr_reader :result
+ # Whether the transition is only existing temporarily for the object
+ attr_writer :transient
+
# Creates a new, specific transition
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
@object = object
@machine = machine
@args = []
+ @transient = false
# Event information
event = machine.events.fetch(event)
@event = event.name
@qualified_event = event.qualified_name
@@ -144,10 +73,12 @@
# To state information
to_state = machine.states.fetch(to_name)
@to = to_state.value
@to_name = to_state.name
@qualified_to_name = to_state.qualified_name
+
+ reset
end
# The attribute which this transition's machine is defined for
def attribute
machine.attribute
@@ -168,10 +99,17 @@
# StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
def loopback?
from_name == to_name
end
+ # Is this transition existing for a short period only? If this is set, it
+ # indicates that the transition (or the event backing it) should not be
+ # written to the object if it fails.
+ def transient?
+ @transient
+ end
+
# A hash of all the core attributes defined for this transition with their
# names as keys and values of the attributes as values.
#
# == Example
#
@@ -201,11 +139,11 @@
def perform(*args)
run_action = [true, false].include?(args.last) ? args.pop : true
self.args = args
# Run the transition
- self.class.perform_within_transaction([self], :action => run_action)
+ !!TransitionCollection.new([self], :actions => run_action).perform
end
# Runs a block within a transaction for the object being transitioned.
# By default, transactions are a no-op unless otherwise defined by the
# machine's integration.
@@ -213,41 +151,43 @@
machine.within_transaction(object) do
yield
end
end
- # Runs the machine's +before+ callbacks for this transition. Only
- # callbacks that are configured to match the event, from state, and to
- # state will be invoked.
+ # Runs the before / after callbacks for this transition. If a block is
+ # provided, then it will be executed between the before and after callbacks.
#
- # Once the callbacks are run, they cannot be run again until this transition
- # is reset.
+ # Configuration options:
+ # * +after+ - Whether to run after callbacks. If false, then any around
+ # callbacks will be paused until called again with +after+ enabled.
+ # Default is true.
#
- # == Example
- #
- # class Vehicle
- # state_machine do
- # before_transition :on => :ignite, :do => lambda {|vehicle| ...}
- # end
- # end
- #
- # vehicle = Vehicle.new
- # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
- # transition.before
- def before
- result = false
+ # This will return true if all before callbacks gets executed. After
+ # callbacks will not have an effect on the result.
+ def run_callbacks(options = {}, &block)
+ options = {:after => true}.merge(options)
+ @success = false
- catch(:halt) do
- unless @before_run
- callback(:before)
- @before_run = true
- end
-
- result = true
+ # Run before callbacks. :halt is caught here so that it rolls up through
+ # any around callbacks.
+ begin
+ halted = !catch(:halt) { before(options[:after], &block); true }
+ rescue Exception => error
+ raise unless @after_block
end
- result
+ # After callbacks are only run if:
+ # * There isn't an after block already running
+ # * An around callback didn't halt after yielding
+ # * They're enabled or the run didn't succeed
+ if @after_block
+ @after_block.call(halted, error)
+ elsif !(@before_run && halted) && (options[:after] || !@success)
+ after
+ end
+
+ @before_run
end
# Transitions the current value of the state to that specified by the
# transition. Once the state is persisted, it cannot be persisted again
# until this transition is reset.
@@ -272,55 +212,10 @@
machine.write(object, :state, to)
@persisted = true
end
end
- # Runs the machine's +after+ callbacks for this transition. Only
- # callbacks that are configured to match the event, from state, and to
- # state will be invoked.
- #
- # The result can be used to indicate whether the associated machine action
- # was executed successfully.
- #
- # Once the callbacks are run, they cannot be run again until this transition
- # is reset.
- #
- # == Halting
- #
- # If any callback throws a <tt>:halt</tt> exception, it will be caught
- # and the callback chain will be automatically stopped. However, this
- # exception will not bubble up to the caller since +after+ callbacks
- # should never halt the execution of a +perform+.
- #
- # == Example
- #
- # class Vehicle
- # state_machine do
- # after_transition :on => :ignite, :do => lambda {|vehicle| ...}
- #
- # event :ignite do
- # transition :parked => :idling
- # end
- # end
- # end
- #
- # vehicle = Vehicle.new
- # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
- # transition.after(true)
- def after(result = nil, success = true)
- @result = result
-
- catch(:halt) do
- unless @after_run
- callback(:after, :success => success)
- @after_run = true
- end
- end
-
- true
- end
-
# Rolls back changes made to the object's state via this transition. This
# will revert the state back to the +from+ value.
#
# == Example
#
@@ -350,10 +245,11 @@
# Resets any tracking of which callbacks have already been run and whether
# the state has already been persisted
def reset
@before_run = @persisted = @after_run = false
+ @around_block = nil
end
# Generates a nicely formatted description of this transitions's contents.
#
# For example,
@@ -362,33 +258,98 @@
# transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
def inspect
"#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
end
- protected
+ private
+ # Runs the machine's +before+ callbacks for this transition. Only
+ # callbacks that are configured to match the event, from state, and to
+ # state will be invoked.
+ #
+ # Once the callbacks are run, they cannot be run again until this transition
+ # is reset.
+ def before(complete = true, index = 0, &block)
+ unless @before_run
+ while callback = machine.callbacks[:before][index]
+ index += 1
+
+ if callback.type == :around
+ # Around callback: need to handle recursively. Execution only gets
+ # paused if:
+ # * The block fails and the callback doesn't run on failures OR
+ # * The block succeeds, but after callbacks are disabled (in which
+ # case a continuation is stored for later execution)
+ return if catch(:pause) do
+ callback.call(object, context, self) do
+ before(complete, index, &block)
+
+ if @success && !complete && !@around_block && !@after_block
+ require 'continuation' unless defined?(callcc)
+ callcc {|block| @around_block = block}
+ end
+
+ throw :pause, true if @around_block && !@after_block || !callback.matches_success?(@success)
+ end
+ end
+ else
+ # Normal before callback
+ callback.call(object, context, self)
+ end
+ end
+
+ @before_run = true
+ end
+
+ action = {:success => true}.merge(block_given? ? yield : {})
+ @result, @success = action[:result], action[:success]
+ end
+
+ # Runs the machine's +after+ callbacks for this transition. Only
+ # callbacks that are configured to match the event, from state, and to
+ # state will be invoked.
+ #
+ # Once the callbacks are run, they cannot be run again until this transition
+ # is reset.
+ #
+ # == Halting
+ #
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
+ # and the callback chain will be automatically stopped. However, this
+ # exception will not bubble up to the caller since +after+ callbacks
+ # should never halt the execution of a +perform+.
+ def after
+ unless @after_run
+ catch(:halt) do
+ # First call any yielded around blocks
+ if @around_block
+ halted, error = callcc do |block|
+ @after_block = block
+ @around_block.call
+ end
+
+ @after_block = @around_block = nil
+ raise error if error
+ throw :halt if halted
+ end
+
+ # Call normal after callbacks in order
+ after_context = context.merge(:success => @success)
+ machine.callbacks[:after].each {|callback| callback.call(object, after_context, self)}
+ end
+
+ @after_run = true
+ end
+ end
+
# Gets a hash of the context defining this unique transition (including
# event, from state, and to state).
#
# == Example
#
# machine = StateMachine.new(Vehicle)
# transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
def context
@context ||= {:on => event, :from => from_name, :to => to_name}
- end
-
- # Runs the callbacks of the given type for this transition. This will
- # only invoke callbacks that exactly match the event, from state, and
- # to state that describe this transition.
- #
- # Additional callback parameters can be specified. By default, this
- # transition is also passed into callbacks.
- def callback(type, context = {})
- context = self.context.merge(context)
-
- machine.callbacks[type].each do |callback|
- callback.call(object, context, self)
- end
end
end
end