state_machines 0.20.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +154 -18
- data/lib/state_machines/branch.rb +30 -17
- data/lib/state_machines/callback.rb +12 -13
- data/lib/state_machines/core.rb +0 -1
- data/lib/state_machines/error.rb +5 -4
- data/lib/state_machines/eval_helpers.rb +178 -49
- data/lib/state_machines/event.rb +31 -32
- data/lib/state_machines/event_collection.rb +4 -5
- data/lib/state_machines/extensions.rb +5 -5
- data/lib/state_machines/helper_module.rb +1 -1
- data/lib/state_machines/integrations/base.rb +1 -1
- data/lib/state_machines/integrations.rb +11 -14
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +25 -11
- data/lib/state_machines/machine/configuration.rb +124 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +75 -619
- data/lib/state_machines/machine_collection.rb +18 -14
- data/lib/state_machines/macro_methods.rb +2 -2
- data/lib/state_machines/matcher.rb +6 -6
- data/lib/state_machines/matcher_helpers.rb +1 -1
- data/lib/state_machines/node_collection.rb +18 -17
- data/lib/state_machines/path.rb +2 -4
- data/lib/state_machines/path_collection.rb +2 -3
- data/lib/state_machines/state.rb +14 -7
- data/lib/state_machines/state_collection.rb +3 -3
- data/lib/state_machines/state_context.rb +6 -7
- data/lib/state_machines/stdio_renderer.rb +16 -16
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +290 -27
- data/lib/state_machines/transition.rb +57 -46
- data/lib/state_machines/transition_collection.rb +22 -25
- data/lib/state_machines/version.rb +1 -1
- metadata +23 -9
@@ -29,17 +29,30 @@ module StateMachines
|
|
29
29
|
# Assert that an object is in a specific state for a given state machine
|
30
30
|
#
|
31
31
|
# @param object [Object] The object with state machines
|
32
|
-
# @param machine_name [Symbol] The name of the state machine
|
33
32
|
# @param expected_state [Symbol] The expected state
|
33
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
34
34
|
# @param message [String, nil] Custom failure message
|
35
35
|
# @return [void]
|
36
36
|
# @raise [AssertionError] If the state doesn't match
|
37
37
|
#
|
38
38
|
# @example
|
39
39
|
# user = User.new
|
40
|
-
#
|
41
|
-
|
42
|
-
|
40
|
+
# assert_sm_state(user, :active) # Uses default :state machine
|
41
|
+
# assert_sm_state(user, :active, machine_name: :status) # Uses :status machine
|
42
|
+
def assert_sm_state(object, expected_state, machine_name: :state, message: nil)
|
43
|
+
name_method = "#{machine_name}_name"
|
44
|
+
|
45
|
+
# Handle the case where machine_name doesn't have a corresponding _name method
|
46
|
+
unless object.respond_to?(name_method)
|
47
|
+
available_machines = begin
|
48
|
+
object.class.state_machines.keys
|
49
|
+
rescue StandardError
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
raise ArgumentError, "No state machine '#{machine_name}' found. Available machines: #{available_machines.inspect}"
|
53
|
+
end
|
54
|
+
|
55
|
+
actual = object.send(name_method)
|
43
56
|
default_message = "Expected #{object.class}##{machine_name} to be #{expected_state}, but was #{actual}"
|
44
57
|
|
45
58
|
if defined?(::Minitest)
|
@@ -55,16 +68,30 @@ module StateMachines
|
|
55
68
|
#
|
56
69
|
# @param object [Object] The object with state machines
|
57
70
|
# @param event [Symbol] The event name
|
71
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
58
72
|
# @param message [String, nil] Custom failure message
|
59
73
|
# @return [void]
|
60
74
|
# @raise [AssertionError] If the transition is not available
|
61
75
|
#
|
62
76
|
# @example
|
63
77
|
# user = User.new
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
|
78
|
+
# assert_sm_can_transition(user, :activate) # Uses default :state machine
|
79
|
+
# assert_sm_can_transition(user, :activate, machine_name: :status) # Uses :status machine
|
80
|
+
def assert_sm_can_transition(object, event, machine_name: :state, message: nil)
|
81
|
+
# Try different method naming patterns
|
82
|
+
possible_methods = [
|
83
|
+
"can_#{event}?", # Default state machine or non-namespaced
|
84
|
+
"can_#{event}_#{machine_name}?" # Namespaced events
|
85
|
+
]
|
86
|
+
|
87
|
+
can_method = possible_methods.find { |method| object.respond_to?(method) }
|
88
|
+
|
89
|
+
unless can_method
|
90
|
+
available_methods = object.methods.grep(/^can_.*\?$/).sort
|
91
|
+
raise ArgumentError, "No transition method found for event :#{event} on machine :#{machine_name}. Available methods: #{available_methods.first(10).inspect}"
|
92
|
+
end
|
93
|
+
|
94
|
+
default_message = "Expected to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned false"
|
68
95
|
|
69
96
|
if defined?(::Minitest)
|
70
97
|
assert object.send(can_method), message || default_message
|
@@ -79,16 +106,30 @@ module StateMachines
|
|
79
106
|
#
|
80
107
|
# @param object [Object] The object with state machines
|
81
108
|
# @param event [Symbol] The event name
|
109
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
82
110
|
# @param message [String, nil] Custom failure message
|
83
111
|
# @return [void]
|
84
112
|
# @raise [AssertionError] If the transition is available
|
85
113
|
#
|
86
114
|
# @example
|
87
115
|
# user = User.new
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
116
|
+
# assert_sm_cannot_transition(user, :delete) # Uses default :state machine
|
117
|
+
# assert_sm_cannot_transition(user, :delete, machine_name: :status) # Uses :status machine
|
118
|
+
def assert_sm_cannot_transition(object, event, machine_name: :state, message: nil)
|
119
|
+
# Try different method naming patterns
|
120
|
+
possible_methods = [
|
121
|
+
"can_#{event}?", # Default state machine or non-namespaced
|
122
|
+
"can_#{event}_#{machine_name}?" # Namespaced events
|
123
|
+
]
|
124
|
+
|
125
|
+
can_method = possible_methods.find { |method| object.respond_to?(method) }
|
126
|
+
|
127
|
+
unless can_method
|
128
|
+
available_methods = object.methods.grep(/^can_.*\?$/).sort
|
129
|
+
raise ArgumentError, "No transition method found for event :#{event} on machine :#{machine_name}. Available methods: #{available_methods.first(10).inspect}"
|
130
|
+
end
|
131
|
+
|
132
|
+
default_message = "Expected not to be able to trigger event :#{event} on #{machine_name}, but #{can_method} returned true"
|
92
133
|
|
93
134
|
if defined?(::Minitest)
|
94
135
|
refute object.send(can_method), message || default_message
|
@@ -103,18 +144,19 @@ module StateMachines
|
|
103
144
|
#
|
104
145
|
# @param object [Object] The object with state machines
|
105
146
|
# @param event [Symbol] The event to trigger
|
106
|
-
# @param machine_name [Symbol] The name of the state machine
|
107
147
|
# @param expected_state [Symbol] The expected state after transition
|
148
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
108
149
|
# @param message [String, nil] Custom failure message
|
109
150
|
# @return [void]
|
110
151
|
# @raise [AssertionError] If the transition fails or results in wrong state
|
111
152
|
#
|
112
153
|
# @example
|
113
154
|
# user = User.new
|
114
|
-
#
|
115
|
-
|
155
|
+
# assert_sm_transition(user, :activate, :active) # Uses default :state machine
|
156
|
+
# assert_sm_transition(user, :activate, :active, machine_name: :status) # Uses :status machine
|
157
|
+
def assert_sm_transition(object, event, expected_state, machine_name: :state, message: nil)
|
116
158
|
object.send("#{event}!")
|
117
|
-
|
159
|
+
assert_sm_state(object, expected_state, machine_name: machine_name, message: message)
|
118
160
|
end
|
119
161
|
|
120
162
|
# === Extended State Machine Assertions ===
|
@@ -205,11 +247,11 @@ module StateMachines
|
|
205
247
|
end
|
206
248
|
alias assert_sm_transition_not_allowed refute_sm_transition_allowed
|
207
249
|
|
208
|
-
def assert_sm_event_triggers(object, event, message = nil)
|
209
|
-
initial_state = object.
|
250
|
+
def assert_sm_event_triggers(object, event, machine_name = :state, message = nil)
|
251
|
+
initial_state = object.send(machine_name)
|
210
252
|
object.send("#{event}!")
|
211
|
-
state_changed = initial_state != object.
|
212
|
-
default_message = "Expected event #{event} to trigger state change"
|
253
|
+
state_changed = initial_state != object.send(machine_name)
|
254
|
+
default_message = "Expected event #{event} to trigger state change on #{machine_name}"
|
213
255
|
|
214
256
|
if defined?(::Minitest)
|
215
257
|
assert state_changed, message || default_message
|
@@ -220,12 +262,12 @@ module StateMachines
|
|
220
262
|
end
|
221
263
|
end
|
222
264
|
|
223
|
-
def refute_sm_event_triggers(object, event, message = nil)
|
224
|
-
initial_state = object.
|
265
|
+
def refute_sm_event_triggers(object, event, machine_name = :state, message = nil)
|
266
|
+
initial_state = object.send(machine_name)
|
225
267
|
begin
|
226
268
|
object.send("#{event}!")
|
227
|
-
state_unchanged = initial_state == object.
|
228
|
-
default_message = "Expected event #{event} to not trigger state change"
|
269
|
+
state_unchanged = initial_state == object.send(machine_name)
|
270
|
+
default_message = "Expected event #{event} to not trigger state change on #{machine_name}"
|
229
271
|
|
230
272
|
if defined?(::Minitest)
|
231
273
|
assert state_unchanged, message || default_message
|
@@ -288,10 +330,26 @@ module StateMachines
|
|
288
330
|
end
|
289
331
|
alias assert_sm_callback_not_executed refute_sm_callback_executed
|
290
332
|
|
291
|
-
|
333
|
+
# Assert that a record's state is persisted correctly for a specific state machine
|
334
|
+
#
|
335
|
+
# @param record [Object] The record to check (should respond to reload)
|
336
|
+
# @param expected [String, Symbol] The expected persisted state
|
337
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
338
|
+
# @param message [String, nil] Custom failure message
|
339
|
+
# @return [void]
|
340
|
+
# @raise [AssertionError] If the persisted state doesn't match
|
341
|
+
#
|
342
|
+
# @example
|
343
|
+
# # Default state machine
|
344
|
+
# assert_sm_state_persisted(user, "active")
|
345
|
+
#
|
346
|
+
# # Specific state machine
|
347
|
+
# assert_sm_state_persisted(ship, "up", :shields)
|
348
|
+
# assert_sm_state_persisted(ship, "armed", :weapons)
|
349
|
+
def assert_sm_state_persisted(record, expected, machine_name = :state, message = nil)
|
292
350
|
record.reload if record.respond_to?(:reload)
|
293
|
-
actual_state = record.
|
294
|
-
default_message = "Expected persisted state #{expected} but got #{actual_state}"
|
351
|
+
actual_state = record.send(machine_name)
|
352
|
+
default_message = "Expected persisted state #{expected} for #{machine_name} but got #{actual_state}"
|
295
353
|
|
296
354
|
if defined?(::Minitest)
|
297
355
|
assert_equal expected, actual_state, message || default_message
|
@@ -301,5 +359,210 @@ module StateMachines
|
|
301
359
|
raise default_message unless expected == actual_state
|
302
360
|
end
|
303
361
|
end
|
362
|
+
|
363
|
+
# Assert that executing a block triggers one or more expected events
|
364
|
+
#
|
365
|
+
# @param object [Object] The object with state machines
|
366
|
+
# @param expected_events [Symbol, Array<Symbol>] The event(s) expected to be triggered
|
367
|
+
# @param machine_name [Symbol] The name of the state machine (defaults to :state)
|
368
|
+
# @param message [String, nil] Custom failure message
|
369
|
+
# @return [void]
|
370
|
+
# @raise [AssertionError] If the expected events were not triggered
|
371
|
+
#
|
372
|
+
# @example
|
373
|
+
# # Single event
|
374
|
+
# assert_sm_triggers_event(vehicle, :crash) { vehicle.redline }
|
375
|
+
#
|
376
|
+
# # Multiple events
|
377
|
+
# assert_sm_triggers_event(vehicle, [:crash, :emergency]) { vehicle.emergency_stop }
|
378
|
+
#
|
379
|
+
# # Specific machine
|
380
|
+
# assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) { vehicle.turn_off_alarm }
|
381
|
+
def assert_sm_triggers_event(object, expected_events, machine_name: :state, message: nil)
|
382
|
+
expected_events = Array(expected_events)
|
383
|
+
triggered_events = []
|
384
|
+
|
385
|
+
# Get the state machine
|
386
|
+
machine = object.class.state_machines[machine_name]
|
387
|
+
raise ArgumentError, "No state machine found for #{machine_name}" unless machine
|
388
|
+
|
389
|
+
# Save original callbacks to restore later
|
390
|
+
machine.callbacks[:before].dup
|
391
|
+
|
392
|
+
# Add a temporary callback to track triggered events
|
393
|
+
temp_callback = machine.before_transition do |_obj, transition|
|
394
|
+
triggered_events << transition.event if transition.event
|
395
|
+
end
|
396
|
+
|
397
|
+
begin
|
398
|
+
# Execute the block
|
399
|
+
yield
|
400
|
+
|
401
|
+
# Check if expected events were triggered
|
402
|
+
missing_events = expected_events - triggered_events
|
403
|
+
extra_events = triggered_events - expected_events
|
404
|
+
|
405
|
+
unless missing_events.empty? && extra_events.empty?
|
406
|
+
default_message = "Expected events #{expected_events.inspect} to be triggered, but got #{triggered_events.inspect}"
|
407
|
+
default_message += ". Missing: #{missing_events.inspect}" if missing_events.any?
|
408
|
+
default_message += ". Extra: #{extra_events.inspect}" if extra_events.any?
|
409
|
+
|
410
|
+
if defined?(::Minitest)
|
411
|
+
assert false, message || default_message
|
412
|
+
elsif defined?(::RSpec)
|
413
|
+
raise message || default_message
|
414
|
+
else
|
415
|
+
raise default_message
|
416
|
+
end
|
417
|
+
end
|
418
|
+
ensure
|
419
|
+
# Restore original callbacks by removing the temporary one
|
420
|
+
machine.callbacks[:before].delete(temp_callback)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# Assert that a before_transition callback is defined with expected arguments
|
425
|
+
#
|
426
|
+
# @param machine_or_class [StateMachines::Machine, Class] The machine or class to check
|
427
|
+
# @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:)
|
428
|
+
# @param message [String, nil] Custom failure message
|
429
|
+
# @return [void]
|
430
|
+
# @raise [AssertionError] If the callback is not defined
|
431
|
+
#
|
432
|
+
# @example
|
433
|
+
# # Check for specific transition callback
|
434
|
+
# assert_before_transition(Vehicle, on: :crash, do: :emergency_stop)
|
435
|
+
#
|
436
|
+
# # Check with from/to states
|
437
|
+
# assert_before_transition(Vehicle.state_machine, from: :parked, to: :idling, do: :start_engine)
|
438
|
+
#
|
439
|
+
# # Check with conditions
|
440
|
+
# assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?)
|
441
|
+
def assert_before_transition(machine_or_class, options = {}, message = nil)
|
442
|
+
_assert_transition_callback(:before, machine_or_class, options, message)
|
443
|
+
end
|
444
|
+
|
445
|
+
# Assert that an after_transition callback is defined with expected arguments
|
446
|
+
#
|
447
|
+
# @param machine_or_class [StateMachines::Machine, Class] The machine or class to check
|
448
|
+
# @param options [Hash] Expected callback options (on:, from:, to:, do:, if:, unless:)
|
449
|
+
# @param message [String, nil] Custom failure message
|
450
|
+
# @return [void]
|
451
|
+
# @raise [AssertionError] If the callback is not defined
|
452
|
+
#
|
453
|
+
# @example
|
454
|
+
# # Check for specific transition callback
|
455
|
+
# assert_after_transition(Vehicle, on: :crash, do: :tow)
|
456
|
+
#
|
457
|
+
# # Check with from/to states
|
458
|
+
# assert_after_transition(Vehicle.state_machine, from: :stalled, to: :parked, do: :log_repair)
|
459
|
+
def assert_after_transition(machine_or_class, options = {}, message = nil)
|
460
|
+
_assert_transition_callback(:after, machine_or_class, options, message)
|
461
|
+
end
|
462
|
+
|
463
|
+
# RSpec-style aliases for event triggering (for consistency with RSpec expectations)
|
464
|
+
alias expect_to_trigger_event assert_sm_triggers_event
|
465
|
+
alias have_triggered_event assert_sm_triggers_event
|
466
|
+
|
467
|
+
private
|
468
|
+
|
469
|
+
# Internal helper for checking transition callbacks
|
470
|
+
def _assert_transition_callback(callback_type, machine_or_class, options, message)
|
471
|
+
# Get the machine
|
472
|
+
machine = machine_or_class.is_a?(StateMachines::Machine) ? machine_or_class : machine_or_class.state_machine
|
473
|
+
raise ArgumentError, 'No state machine found' unless machine
|
474
|
+
|
475
|
+
callbacks = machine.callbacks[callback_type] || []
|
476
|
+
|
477
|
+
# Extract expected conditions
|
478
|
+
expected_event = options[:on]
|
479
|
+
expected_from = options[:from]
|
480
|
+
expected_to = options[:to]
|
481
|
+
expected_method = options[:do]
|
482
|
+
expected_if = options[:if]
|
483
|
+
expected_unless = options[:unless]
|
484
|
+
|
485
|
+
# Find matching callback
|
486
|
+
matching_callback = callbacks.find do |callback|
|
487
|
+
branch = callback.branch
|
488
|
+
|
489
|
+
# Check event requirement
|
490
|
+
if expected_event
|
491
|
+
event_requirement = branch.event_requirement
|
492
|
+
event_matches = if event_requirement && event_requirement.respond_to?(:values)
|
493
|
+
event_requirement.values.include?(expected_event)
|
494
|
+
else
|
495
|
+
false
|
496
|
+
end
|
497
|
+
next false unless event_matches
|
498
|
+
end
|
499
|
+
|
500
|
+
# Check state requirements (from/to)
|
501
|
+
if expected_from || expected_to
|
502
|
+
state_matches = false
|
503
|
+
branch.state_requirements.each do |req|
|
504
|
+
from_matches = !expected_from || (req[:from] && req[:from].respond_to?(:values) && req[:from].values.include?(expected_from))
|
505
|
+
to_matches = !expected_to || (req[:to] && req[:to].respond_to?(:values) && req[:to].values.include?(expected_to))
|
506
|
+
|
507
|
+
if from_matches && to_matches
|
508
|
+
state_matches = true
|
509
|
+
break
|
510
|
+
end
|
511
|
+
end
|
512
|
+
next false unless state_matches
|
513
|
+
end
|
514
|
+
|
515
|
+
# Check method requirement
|
516
|
+
if expected_method
|
517
|
+
methods = callback.instance_variable_get(:@methods) || []
|
518
|
+
method_matches = methods.any? do |method|
|
519
|
+
(method.is_a?(Symbol) && method == expected_method) ||
|
520
|
+
(method.is_a?(String) && method.to_sym == expected_method) ||
|
521
|
+
(method.respond_to?(:call) && method.respond_to?(:source_location))
|
522
|
+
end
|
523
|
+
next false unless method_matches
|
524
|
+
end
|
525
|
+
|
526
|
+
# Check if condition
|
527
|
+
if expected_if
|
528
|
+
if_condition = branch.if_condition
|
529
|
+
if_matches = (if_condition.is_a?(Symbol) && if_condition == expected_if) ||
|
530
|
+
(if_condition.is_a?(String) && if_condition.to_sym == expected_if) ||
|
531
|
+
if_condition.respond_to?(:call)
|
532
|
+
next false unless if_matches
|
533
|
+
end
|
534
|
+
|
535
|
+
# Check unless condition
|
536
|
+
if expected_unless
|
537
|
+
unless_condition = branch.unless_condition
|
538
|
+
unless_matches = (unless_condition.is_a?(Symbol) && unless_condition == expected_unless) ||
|
539
|
+
(unless_condition.is_a?(String) && unless_condition.to_sym == expected_unless) ||
|
540
|
+
unless_condition.respond_to?(:call)
|
541
|
+
next false unless unless_matches
|
542
|
+
end
|
543
|
+
|
544
|
+
true
|
545
|
+
end
|
546
|
+
|
547
|
+
return if matching_callback
|
548
|
+
|
549
|
+
expected_parts = []
|
550
|
+
expected_parts << "on: #{expected_event.inspect}" if expected_event
|
551
|
+
expected_parts << "from: #{expected_from.inspect}" if expected_from
|
552
|
+
expected_parts << "to: #{expected_to.inspect}" if expected_to
|
553
|
+
expected_parts << "do: #{expected_method.inspect}" if expected_method
|
554
|
+
expected_parts << "if: #{expected_if.inspect}" if expected_if
|
555
|
+
expected_parts << "unless: #{expected_unless.inspect}" if expected_unless
|
556
|
+
|
557
|
+
default_message = "Expected #{callback_type}_transition callback with #{expected_parts.join(', ')} to be defined, but it was not found"
|
558
|
+
|
559
|
+
if defined?(::Minitest)
|
560
|
+
assert false, message || default_message
|
561
|
+
elsif defined?(::RSpec)
|
562
|
+
raise message || default_message
|
563
|
+
else
|
564
|
+
raise default_message
|
565
|
+
end
|
566
|
+
end
|
304
567
|
end
|
305
568
|
end
|
@@ -33,11 +33,11 @@ module StateMachines
|
|
33
33
|
# Determines whether the current ruby implementation supports pausing and
|
34
34
|
# resuming transitions
|
35
35
|
def self.pause_supported?
|
36
|
-
%w
|
36
|
+
%w[ruby maglev].include?(RUBY_ENGINE)
|
37
37
|
end
|
38
38
|
|
39
39
|
# Creates a new, specific transition
|
40
|
-
def initialize(object, machine, event, from_name, to_name, read_state = true)
|
40
|
+
def initialize(object, machine, event, from_name, to_name, read_state = true) # :nodoc:
|
41
41
|
@object = object
|
42
42
|
@machine = machine
|
43
43
|
@args = []
|
@@ -136,7 +136,7 @@ module StateMachines
|
|
136
136
|
# transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
137
137
|
# transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
|
138
138
|
def attributes
|
139
|
-
@attributes ||= {object: object, attribute: attribute, event: event, from: from, to: to}
|
139
|
+
@attributes ||= { object: object, attribute: attribute, event: event, from: from, to: to }
|
140
140
|
end
|
141
141
|
|
142
142
|
# Runs the actual transition and any before/after callbacks associated
|
@@ -153,25 +153,32 @@ module StateMachines
|
|
153
153
|
#
|
154
154
|
# vehicle = Vehicle.new
|
155
155
|
# transition = StateMachines::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
156
|
-
# transition.perform
|
157
|
-
# transition.perform(false)
|
158
|
-
# transition.perform(
|
159
|
-
# transition.perform(Time.now
|
156
|
+
# transition.perform # => Runs the +save+ action after setting the state attribute
|
157
|
+
# transition.perform(false) # => Only sets the state attribute
|
158
|
+
# transition.perform(run_action: false) # => Only sets the state attribute
|
159
|
+
# transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
|
160
|
+
# transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
|
161
|
+
# transition.perform(Time.now, run_action: false) # => Passes in additional arguments and only sets the state attribute
|
160
162
|
def perform(*args)
|
161
|
-
run_action =
|
163
|
+
run_action = true
|
164
|
+
|
165
|
+
if [true, false].include?(args.last)
|
166
|
+
run_action = args.pop
|
167
|
+
elsif args.last.is_a?(Hash) && args.last.key?(:run_action)
|
168
|
+
run_action = args.last.delete(:run_action)
|
169
|
+
end
|
170
|
+
|
162
171
|
self.args = args
|
163
172
|
|
164
173
|
# Run the transition
|
165
|
-
!!TransitionCollection.new([self], {use_transactions: machine.use_transactions, actions: run_action}).perform
|
174
|
+
!!TransitionCollection.new([self], { use_transactions: machine.use_transactions, actions: run_action }).perform
|
166
175
|
end
|
167
176
|
|
168
177
|
# Runs a block within a transaction for the object being transitioned.
|
169
178
|
# By default, transactions are a no-op unless otherwise defined by the
|
170
179
|
# machine's integration.
|
171
|
-
def within_transaction
|
172
|
-
machine.within_transaction(object)
|
173
|
-
yield
|
174
|
-
end
|
180
|
+
def within_transaction(&)
|
181
|
+
machine.within_transaction(object, &)
|
175
182
|
end
|
176
183
|
|
177
184
|
# Runs the before / after callbacks for this transition. If a block is
|
@@ -186,7 +193,7 @@ module StateMachines
|
|
186
193
|
# This will return true if all before callbacks gets executed. After
|
187
194
|
# callbacks will not have an effect on the result.
|
188
195
|
def run_callbacks(options = {}, &block)
|
189
|
-
options = {before: true, after: true}.merge(options)
|
196
|
+
options = { before: true, after: true }.merge(options)
|
190
197
|
@success = false
|
191
198
|
|
192
199
|
halted = pausable { before(options[:after], &block) } if options[:before]
|
@@ -219,10 +226,10 @@ module StateMachines
|
|
219
226
|
#
|
220
227
|
# vehicle.state # => 'idling'
|
221
228
|
def persist
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
229
|
+
return if @persisted
|
230
|
+
|
231
|
+
machine.write(object, :state, to)
|
232
|
+
@persisted = true
|
226
233
|
end
|
227
234
|
|
228
235
|
# Rolls back changes made to the object's state via this transition. This
|
@@ -265,11 +272,11 @@ module StateMachines
|
|
265
272
|
# and event involved in the transition are equal
|
266
273
|
def ==(other)
|
267
274
|
other.instance_of?(self.class) &&
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
275
|
+
other.object == object &&
|
276
|
+
other.machine == machine &&
|
277
|
+
other.from_name == from_name &&
|
278
|
+
other.to_name == to_name &&
|
279
|
+
other.event == event
|
273
280
|
end
|
274
281
|
|
275
282
|
# Generates a nicely formatted description of this transitions's contents.
|
@@ -279,10 +286,10 @@ module StateMachines
|
|
279
286
|
# transition = StateMachines::Transition.new(object, machine, :ignite, :parked, :idling)
|
280
287
|
# transition # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
281
288
|
def inspect
|
282
|
-
"#<#{self.class} #{%w
|
289
|
+
"#<#{self.class} #{%w[attribute event from from_name to to_name].map { |attr| "#{attr}=#{send(attr).inspect}" } * ' '}>"
|
283
290
|
end
|
284
291
|
|
285
|
-
|
292
|
+
private
|
286
293
|
|
287
294
|
# Runs a block that may get paused. If the block doesn't pause, then
|
288
295
|
# execution will continue as normal. If the block gets paused, then it
|
@@ -292,13 +299,16 @@ module StateMachines
|
|
292
299
|
# getting paused.
|
293
300
|
def pausable
|
294
301
|
begin
|
295
|
-
halted = !catch(:halt)
|
296
|
-
|
302
|
+
halted = !catch(:halt) do
|
303
|
+
yield
|
304
|
+
true
|
305
|
+
end
|
306
|
+
rescue StandardError => e
|
297
307
|
raise unless @resume_block
|
298
308
|
end
|
299
309
|
|
300
310
|
if @resume_block
|
301
|
-
@resume_block.call(halted,
|
311
|
+
@resume_block.call(halted, e)
|
302
312
|
else
|
303
313
|
halted
|
304
314
|
end
|
@@ -310,12 +320,12 @@ module StateMachines
|
|
310
320
|
def pause
|
311
321
|
raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported?
|
312
322
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
323
|
+
return if @resume_block
|
324
|
+
|
325
|
+
require 'continuation' unless defined?(callcc)
|
326
|
+
callcc do |block|
|
327
|
+
@paused_block = block
|
328
|
+
throw :halt, true
|
319
329
|
end
|
320
330
|
end
|
321
331
|
|
@@ -372,8 +382,9 @@ module StateMachines
|
|
372
382
|
@before_run = true
|
373
383
|
end
|
374
384
|
|
375
|
-
action = {success: true}.merge(block_given? ? yield : {})
|
376
|
-
@result
|
385
|
+
action = { success: true }.merge(block_given? ? yield : {})
|
386
|
+
@result = action[:result]
|
387
|
+
@success = action[:success]
|
377
388
|
end
|
378
389
|
|
379
390
|
# Runs the machine's +after+ callbacks for this transition. Only
|
@@ -390,17 +401,17 @@ module StateMachines
|
|
390
401
|
# exception will not bubble up to the caller since +after+ callbacks
|
391
402
|
# should never halt the execution of a +perform+.
|
392
403
|
def after
|
393
|
-
|
394
|
-
# First resume previously paused callbacks
|
395
|
-
if resume
|
396
|
-
catch(:halt) do
|
397
|
-
type = @success ? :after : :failure
|
398
|
-
machine.callbacks[type].each { |callback| callback.call(object, context, self) }
|
399
|
-
end
|
400
|
-
end
|
404
|
+
return if @after_run
|
401
405
|
|
402
|
-
|
406
|
+
# First resume previously paused callbacks
|
407
|
+
if resume
|
408
|
+
catch(:halt) do
|
409
|
+
type = @success ? :after : :failure
|
410
|
+
machine.callbacks[type].each { |callback| callback.call(object, context, self) }
|
411
|
+
end
|
403
412
|
end
|
413
|
+
|
414
|
+
@after_run = true
|
404
415
|
end
|
405
416
|
|
406
417
|
# Gets a hash of the context defining this unique transition (including
|
@@ -412,7 +423,7 @@ module StateMachines
|
|
412
423
|
# transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
413
424
|
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
|
414
425
|
def context
|
415
|
-
@context ||= {on: event, from: from_name, to: to_name}
|
426
|
+
@context ||= { on: event, from: from_name, to: to_name }
|
416
427
|
end
|
417
428
|
end
|
418
429
|
end
|