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