Class: StateMachines::Transition

Inherits:
Object
  • Object
show all
Defined in:
lib/state_machines/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

Instance Method Summary collapse

Constructor Details

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

Creates a new, specific transition



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/state_machines/transition.rb', line 39

def initialize(object, machine, event, from_name, to_name, read_state = true) # :nodoc:
  @object = object
  @machine = machine
  @args = []
  @transient = false
  @paused_fiber = nil
  @resuming = false
  @continuation_block = nil

  @event = machine.events.fetch(event)
  @from_state = machine.states.fetch(from_name)
  @from = read_state ? machine.read(object, :state) : @from_state.value
  @to_state = machine.states.fetch(to_name)
  @to = @to_state.value

  reset
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)



30
31
32
# File 'lib/state_machines/transition.rb', line 30

def args
  @args
end

#fromObject (readonly)

The original state value before the transition



23
24
25
# File 'lib/state_machines/transition.rb', line 23

def from
  @from
end

#machineObject (readonly)

The state machine for which this transition is defined



20
21
22
# File 'lib/state_machines/transition.rb', line 20

def machine
  @machine
end

#objectObject (readonly)

The object being transitioned



17
18
19
# File 'lib/state_machines/transition.rb', line 17

def object
  @object
end

#resultObject (readonly)

The result of invoking the action associated with the machine



33
34
35
# File 'lib/state_machines/transition.rb', line 33

def result
  @result
end

#toObject (readonly)

The new state value after the transition



26
27
28
# File 'lib/state_machines/transition.rb', line 26

def to
  @to
end

#transient=(value) ⇒ Object (writeonly)

Whether the transition is only existing temporarily for the object



36
37
38
# File 'lib/state_machines/transition.rb', line 36

def transient=(value)
  @transient = value
end

Instance Method Details

#==(other) ⇒ Object

Determines equality of transitions by testing whether the object, states, and event involved in the transition are equal



295
296
297
298
299
300
301
302
# File 'lib/state_machines/transition.rb', line 295

def ==(other)
  other.instance_of?(self.class) &&
    other.object == object &&
    other.machine == machine &&
    other.from_name == from_name &&
    other.to_name == to_name &&
    other.event == event
end

#actionObject

The action that will be run when this transition is performed



63
64
65
# File 'lib/state_machines/transition.rb', line 63

def action
  machine.action
end

#attributeObject

The attribute which this transition’s machine is defined for



58
59
60
# File 'lib/state_machines/transition.rb', line 58

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 = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
transition.attributes   # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}


139
140
141
# File 'lib/state_machines/transition.rb', line 139

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

#eventObject

The event that triggered the transition



68
69
70
# File 'lib/state_machines/transition.rb', line 68

def event
  @event.name
end

#from_nameObject

The state name before the transition



83
84
85
# File 'lib/state_machines/transition.rb', line 83

def from_name
  @from_state.name
end

#human_eventObject

The human-readable name of the event that triggered the transition



78
79
80
# File 'lib/state_machines/transition.rb', line 78

def human_event
  @event.human_name(@object.class)
end

#human_from_nameObject

The human-readable state name before the transition



93
94
95
# File 'lib/state_machines/transition.rb', line 93

def human_from_name
  @from_state.human_name(@object.class)
end

#human_to_nameObject

The new human-readable state name after the transition



108
109
110
# File 'lib/state_machines/transition.rb', line 108

def human_to_name
  @to_state.human_name(@object.class)
end

#inspectObject

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

For example,

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


310
311
312
# File 'lib/state_machines/transition.rb', line 310

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)
StateMachines::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback?   # => true
StateMachines::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback?   # => false

Returns:

  • (Boolean)


120
121
122
# File 'lib/state_machines/transition.rb', line 120

def loopback?
  from_name == to_name
end

#paused?Boolean

Checks whether this transition is currently paused. Returns true if there is a paused fiber, false otherwise.

Returns:

  • (Boolean)


316
317
318
# File 'lib/state_machines/transition.rb', line 316

def paused?
  @paused_fiber&.alive? || false
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 = StateMachines::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
transition.perform(run_action: false)           # => Only sets the state attribute
transition.perform(Time.now)                    # => Passes in additional arguments and runs the +save+ action
transition.perform(Time.now, false)             # => Passes in additional arguments and only sets the state attribute
transition.perform(Time.now, run_action: false) # => Passes in additional arguments and only sets the state attribute


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/state_machines/transition.rb', line 163

def perform(*args)
  run_action = case args.last
               in true | false
                 args.pop
               in { run_action: }
                 args.last.delete(:run_action)
               else
                 true
               end

  self.args = args

  # Run the transition
  !!TransitionCollection.new([self], { use_transactions: machine.use_transactions, actions: run_action }).perform
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 = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
transition.persist

vehicle.state   # => 'idling'


248
249
250
251
252
253
# File 'lib/state_machines/transition.rb', line 248

def persist
  return if @persisted

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

#qualified_eventObject

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



73
74
75
# File 'lib/state_machines/transition.rb', line 73

def qualified_event
  @event.qualified_name
end

#qualified_from_nameObject

The fully-qualified state name before the transition



88
89
90
# File 'lib/state_machines/transition.rb', line 88

def qualified_from_name
  @from_state.qualified_name
end

#qualified_to_nameObject

The new fully-qualified state name after the transition



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

def qualified_to_name
  @to_state.qualified_name
end

#resetObject

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



286
287
288
289
290
291
# File 'lib/state_machines/transition.rb', line 286

def reset
  @before_run = @persisted = @after_run = false
  @paused_fiber = nil
  @resuming = false
  @continuation_block = nil
end

#resumable?Boolean

Checks whether this transition has a paused fiber that can be resumed. Returns true if there is a paused fiber, false otherwise.

Note: The actual resuming happens automatically when run_callbacks is called again on a transition with a paused fiber.

Returns:

  • (Boolean)


325
326
327
# File 'lib/state_machines/transition.rb', line 325

def resumable?
  paused?
end

#resume!(&block) ⇒ Object

Manually resumes the execution of a previously paused callback. Returns true if the transition was successfully resumed and completed, false if there was no paused fiber, and raises an exception if the transition was halted.



333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/state_machines/transition.rb', line 333

def resume!(&block)
  return false unless paused?

  # Store continuation block if provided
  @continuation_block = block if block_given?

  # Run the pausable block which will resume the fiber
  halted = pausable { true }

  # Return whether the transition completed successfully
  !halted
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 = StateMachines::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"


279
280
281
282
# File 'lib/state_machines/transition.rb', line 279

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

#run_callbacks(options = {}, &block) ⇒ Object

Runs the before / after callbacks for this transition. If a block is provided, then it will be executed between the before and after callbacks.

Configuration options:

  • before - Whether to run before callbacks.

  • after - Whether to run after callbacks. If false, then any around callbacks will be paused until called again with after enabled. Default is true.

This will return true if all before callbacks gets executed. After callbacks will not have an effect on the result.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/state_machines/transition.rb', line 197

def run_callbacks(options = {}, &block)
  options = { before: true, after: true }.merge(options)

  # If we have a paused fiber and we're not trying to resume (after: false),
  # this is an idempotent call on an already-paused transition. Just return true.
  return true if @paused_fiber&.alive? && !options[:after]

  # Extract pausable options
  pausable_options = options.key?(:fiber) ? { fiber: options[:fiber] } : {}

  # Check if we're resuming from a pause
  if @paused_fiber&.alive? && options[:after]
    # Resume the paused fiber
    # Don't reset @success when resuming - preserve the state from the pause
    # Store the block for later execution
    @continuation_block = block if block_given?
    halted = pausable(pausable_options) { true }
  else
    @success = false
    # For normal execution (not pause/resume), default to success
    # The action block will override this if needed
    halted = pausable(pausable_options) { before(options[:after], &block) } if options[:before]
  end

  # After callbacks are only run if:
  # * An around callback didn't halt after yielding OR the run failed
  # * They're enabled or the run didn't succeed
  after if (!(@before_run && halted) || !@success) && (options[:after] || !@success)

  @before_run
end

#to_nameObject

The new state name after the transition



98
99
100
# File 'lib/state_machines/transition.rb', line 98

def to_name
  @to_state.name
end

#transient?Boolean

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.

Returns:

  • (Boolean)


127
128
129
# File 'lib/state_machines/transition.rb', line 127

def transient?
  @transient
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.



182
183
184
# File 'lib/state_machines/transition.rb', line 182

def within_transaction(&)
  machine.within_transaction(object, &)
end