lib/state_machine/transition.rb in state_machine-0.6.3 vs lib/state_machine/transition.rb in state_machine-0.7.0

- old
+ new

@@ -8,53 +8,177 @@ # 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 + + # Always run after callbacks regardless of whether the actions failed + transitions.each {|transition| transition.after(results[transition.action])} unless options[:after] == false + + # 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 # The event that triggered the transition attr_reader :event + # The fully-qualified name of the event that triggered the transition + attr_reader :qualified_event + # The original state value *before* the transition attr_reader :from # The original state name *before* the transition attr_reader :from_name + # The original fully-qualified state name *before* transition + attr_reader :qualified_from_name + # The new state value *after* the transition attr_reader :to # The new state name *after* the transition attr_reader :to_name + # The new fully-qualified state name *after* the transition + attr_reader :qualified_to_name + + # The arguments passed in to the event that triggered the transition + # (does not include the +run_action+ boolean argument if specified) + attr_accessor :args + + # The result of invoking the action associated with the machine + attr_reader :result + # Creates a new, specific transition def initialize(object, machine, event, from_name, to_name) #:nodoc: @object = object @machine = machine - @event = event - @from = object.send(machine.attribute) - @from_name = from_name - @to = machine.states[to_name].value - @to_name = to_name + @args = [] + + # Event information (no-ops don't have events) + if event + event = machine.events.fetch(event) + @event = event.name + @qualified_event = event.qualified_name + end + + # From state information + from_state = machine.states.fetch(from_name) + @from = machine.read(object) + @from_name = from_state.name + @qualified_from_name = from_state.qualified_name + + # 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 end - # Gets the attribute which this transition's machine is defined for + # The attribute which this transition's machine is defined for def attribute machine.attribute end - # Gets a hash of all the core attributes defined for this transition with - # their names as keys and values of the attributes as values. + # The action that will be run when this transition is performed + def action + machine.action + end + + # Does this transition represent a loopback (i.e. the from and to state + # are the same) # # == Example # # machine = StateMachine.new(Vehicle) + # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true + # StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false + def loopback? + from_name == to_name + 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 + # + # machine = StateMachine.new(Vehicle) # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling) # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'} def attributes @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to} end @@ -73,35 +197,142 @@ # # vehicle = Vehicle.new # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling) # transition.perform # => Runs the +save+ action after setting the state attribute # transition.perform(false) # => Only sets the state attribute - def perform(run_action = true) - result = false + 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) + 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. + def within_transaction machine.within_transaction(object) do - catch(:halt) do - # Run before callbacks - callback(:before) - - # Updates the object's attribute to the ending state - object.send("#{attribute}=", to) - result = run_action && machine.action ? object.send(machine.action) != false : true - - # Always run after callbacks regardless of whether the action failed. - # Result is included in case the callback depends on this value - callback(:after, result) - end - - # Make sure the transaction gets the correct return value for determining - # whether it should rollback or not - result = result != false + 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. + # + # == 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 + catch(:halt) do + callback(:before) + result = true + end + result end + # Transitions the current value of the state to that specified by the + # transition. + # + # == Example + # + # class Vehicle + # state_machine do + # event :ignite do + # transition :parked => :idling + # end + # end + # end + # + # vehicle = Vehicle.new + # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling) + # transition.persist + # + # vehicle.state # => 'idling' + def persist + machine.write(object, to) + 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 is used to indicate whether the associated machine action + # was executed successfully. + # + # == 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) + @result = result + + catch(:halt) do + callback(:after) + 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 + # + # class Vehicle + # state_machine :initial => :parked do + # event :ignite do + # transition :parked => :idling + # end + # end + # end + # + # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked"> + # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling) + # + # # Persist the new state + # vehicle.state # => "parked" + # transition.persist + # vehicle.state # => "idling" + # + # # Roll back to the original state + # transition.rollback + # vehicle.state # => "parked" + def rollback + machine.write(object, from) + end + # Generates a nicely formatted description of this transitions's contents. # # For example, # # transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling) @@ -127,12 +358,12 @@ # 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, *args) + def callback(type) machine.callbacks[type].each do |callback| - callback.call(object, context, self, *args) + callback.call(object, context, self) end end end end