Class: StateMachine::Transition

Inherits:
Object
  • Object
show all
Defined in:
lib/state_machine/transition.rb

Overview

A transition represents a state change for a specific attribute.

Transitions consist of:

  • An event

  • A starting state

  • An ending state

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(object, machine, event, from_name, to_name, read_state = true) ⇒ Transition

Creates a new, specific transition



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/state_machine/transition.rb', line 128

def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
  @object = object
  @machine = machine
  @args = []
  
  # Event information
  event = machine.events.fetch(event)
  @event = event.name
  @qualified_event = event.qualified_name
  
  # From state information
  from_state = machine.states.fetch(from_name)
  @from = read_state ? machine.read(object, :state) : from_state.value
  @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

Instance Attribute Details

#argsObject

The arguments passed in to the event that triggered the transition (does not include the run_action boolean argument if specified)



122
123
124
# File 'lib/state_machine/transition.rb', line 122

def args
  @args
end

#eventObject (readonly)

The event that triggered the transition



97
98
99
# File 'lib/state_machine/transition.rb', line 97

def event
  @event
end

#fromObject (readonly)

The original state value before the transition



103
104
105
# File 'lib/state_machine/transition.rb', line 103

def from
  @from
end

#from_nameObject (readonly)

The original state name before the transition



106
107
108
# File 'lib/state_machine/transition.rb', line 106

def from_name
  @from_name
end

#machineObject (readonly)

The state machine for which this transition is defined



94
95
96
# File 'lib/state_machine/transition.rb', line 94

def machine
  @machine
end

#objectObject (readonly)

The object being transitioned



91
92
93
# File 'lib/state_machine/transition.rb', line 91

def object
  @object
end

#qualified_eventObject (readonly)

The fully-qualified name of the event that triggered the transition



100
101
102
# File 'lib/state_machine/transition.rb', line 100

def qualified_event
  @qualified_event
end

#qualified_from_nameObject (readonly)

The original fully-qualified state name before transition



109
110
111
# File 'lib/state_machine/transition.rb', line 109

def qualified_from_name
  @qualified_from_name
end

#qualified_to_nameObject (readonly)

The new fully-qualified state name after the transition



118
119
120
# File 'lib/state_machine/transition.rb', line 118

def qualified_to_name
  @qualified_to_name
end

#resultObject (readonly)

The result of invoking the action associated with the machine



125
126
127
# File 'lib/state_machine/transition.rb', line 125

def result
  @result
end

#toObject (readonly)

The new state value after the transition



112
113
114
# File 'lib/state_machine/transition.rb', line 112

def to
  @to
end

#to_nameObject (readonly)

The new state name after the transition



115
116
117
# File 'lib/state_machine/transition.rb', line 115

def to_name
  @to_name
end

Class Method Details

.perform(transitions, options = {}) ⇒ Object

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:

  • :action - Whether to run the action configured for each transition

  • :after - 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.

Raises:

  • (ArgumentError)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/state_machine/transition.rb', line 28

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

.perform_within_transaction(transitions, options = {}) ⇒ Object

Runs one or more transitions within a transaction. See StateMachine::Transition.perform for more information.



80
81
82
83
84
85
86
87
# File 'lib/state_machine/transition.rb', line 80

def perform_within_transaction(transitions, options = {})
  success = false
  transitions.first.within_transaction do
    success = perform(transitions, options)
  end
  
  success
end

Instance Method Details

#actionObject

The action that will be run when this transition is performed



157
158
159
# File 'lib/state_machine/transition.rb', line 157

def action
  machine.action
end

#after(result = nil, success = true) ⇒ Object

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 :halt 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)


309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/state_machine/transition.rb', line 309

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

#attributeObject

The attribute which this transition’s machine is defined for



152
153
154
# File 'lib/state_machine/transition.rb', line 152

def attribute
  machine.attribute
end

#attributesObject

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'}


181
182
183
# File 'lib/state_machine/transition.rb', line 181

def attributes
  @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
end

#beforeObject

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.

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


236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/state_machine/transition.rb', line 236

def before
  result = false
  
  catch(:halt) do
    unless @before_run
      callback(:before)
      @before_run = true
    end
    
    result = true
  end
  
  result
end

#inspectObject

Generates a nicely formatted description of this transitions’s contents.

For example,

transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
transition   # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>


363
364
365
# File 'lib/state_machine/transition.rb', line 363

def inspect
  "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
end

#loopback?Boolean

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

Returns:

  • (Boolean)


169
170
171
# File 'lib/state_machine/transition.rb', line 169

def loopback?
  from_name == to_name
end

#perform(*args) ⇒ Object

Runs the actual transition and any before/after callbacks associated with the transition. The action associated with the transition/machine can be skipped by passing in false.

Examples

class Vehicle
  state_machine :action => :save do
    ...
  end
end

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


201
202
203
204
205
206
207
# File 'lib/state_machine/transition.rb', line 201

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

#persistObject

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.

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'


270
271
272
273
274
275
# File 'lib/state_machine/transition.rb', line 270

def persist
  unless @persisted
    machine.write(object, :state, to)
    @persisted = true
  end
end

#resetObject

Resets any tracking of which callbacks have already been run and whether the state has already been persisted



353
354
355
# File 'lib/state_machine/transition.rb', line 353

def reset
  @before_run = @persisted = @after_run = false
end

#rollbackObject

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"


346
347
348
349
# File 'lib/state_machine/transition.rb', line 346

def rollback
  reset
  machine.write(object, :state, from)
end

#within_transactionObject

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.



212
213
214
215
216
# File 'lib/state_machine/transition.rb', line 212

def within_transaction
  machine.within_transaction(object) do
    yield
  end
end