state_machines 0.200.0 → 0.202.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/lib/state_machines/async_mode/async_events.rb +8 -4
- data/lib/state_machines/machine/utilities.rb +27 -0
- data/lib/state_machines/machine.rb +2 -3
- data/lib/state_machines/state.rb +3 -3
- data/lib/state_machines/transition.rb +23 -8
- data/lib/state_machines/transition_collection.rb +27 -0
- data/lib/state_machines/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e579c5afa633f1402bfd1e1a5ebd0c446ff410eb3b06940d74dce0d0e024f530
|
|
4
|
+
data.tar.gz: 629c2f403c71e966e98c5d35c64fa8ffa99caaa8e5aced5e205a569eb867e811
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bf56f62b3f6e495fc799ba48d499c0bb7325dfdb3c24d1f8aafc2c9daaade581bef63495e8562a633ee32d2ca3870e8e8131052d97bb152c793ab86f0a3c0fb
|
|
7
|
+
data.tar.gz: f2ae6e891727b60a4310160fbfa7044b4b949a8bccb9f8f80d7d3733dda6e528bf14f3449e7ab420b35d84ef971a6e6349e43a162e16b735e0be61e2ae7571be
|
|
@@ -114,8 +114,7 @@ module StateMachines
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
Async do
|
|
117
|
-
|
|
118
|
-
machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
|
|
117
|
+
fire_with_bang(machine, event_name, *args)
|
|
119
118
|
end
|
|
120
119
|
end
|
|
121
120
|
|
|
@@ -140,11 +139,11 @@ module StateMachines
|
|
|
140
139
|
|
|
141
140
|
if defined?(::Async::Task) && ::Async::Task.current?
|
|
142
141
|
# Already in async context, just fire directly with bang behavior
|
|
143
|
-
machine
|
|
142
|
+
fire_with_bang(machine, event_name, *args)
|
|
144
143
|
else
|
|
145
144
|
# Create async context and wait for result (may raise exception)
|
|
146
145
|
Async do
|
|
147
|
-
machine
|
|
146
|
+
fire_with_bang(machine, event_name, *args)
|
|
148
147
|
end.wait
|
|
149
148
|
end
|
|
150
149
|
end
|
|
@@ -199,6 +198,11 @@ module StateMachines
|
|
|
199
198
|
|
|
200
199
|
private
|
|
201
200
|
|
|
201
|
+
# Fires the event on the given machine, raising InvalidTransition on failure
|
|
202
|
+
def fire_with_bang(machine, event_name, *)
|
|
203
|
+
machine.events[event_name].fire(self, *) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
|
|
204
|
+
end
|
|
205
|
+
|
|
202
206
|
# Check if this event method should have async versions
|
|
203
207
|
def async_method_for_event?(event_method)
|
|
204
208
|
# Find which machine contains this event
|
|
@@ -55,6 +55,33 @@ module StateMachines
|
|
|
55
55
|
target.method_defined?(method) || target.private_method_defined?(method)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Generates the warning message for a method conflict, including where
|
|
59
|
+
# the conflicting method was defined (when it has a Ruby source
|
|
60
|
+
# location) and which class the state machine is being defined on
|
|
61
|
+
def method_conflict_message(scope, method, defined_in)
|
|
62
|
+
scope_label = scope == :class ? 'Class' : 'Instance'
|
|
63
|
+
defined_in_name = defined_in.name && !defined_in.name.empty? ? defined_in.name : defined_in.to_s
|
|
64
|
+
location = conflicting_method_location(scope, method, defined_in)
|
|
65
|
+
location_label = location ? " at #{location[0]}:#{location[1]}" : ''
|
|
66
|
+
|
|
67
|
+
"#{scope_label} method \"#{method}\" is already defined in #{defined_in_name}#{location_label}, " \
|
|
68
|
+
'use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true. ' \
|
|
69
|
+
"Defining #{name.inspect} state machine on #{owner_class}."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Looks up the source location of the conflicting method. Returns nil
|
|
73
|
+
# for methods without a Ruby source (e.g. C-defined methods like
|
|
74
|
+
# Kernel#fail) and for the machine's own helper modules, where the
|
|
75
|
+
# location would just point inside this gem
|
|
76
|
+
def conflicting_method_location(scope, method, defined_in)
|
|
77
|
+
return if @helper_modules.value?(defined_in)
|
|
78
|
+
|
|
79
|
+
target = scope == :class && defined_in.is_a?(Class) ? defined_in.singleton_class : defined_in
|
|
80
|
+
target.instance_method(method).source_location
|
|
81
|
+
rescue NameError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
58
85
|
# Pluralizes the given word using #pluralize (if available) or simply
|
|
59
86
|
# adding an "s" to the end of the word
|
|
60
87
|
def pluralize(word)
|
|
@@ -355,7 +355,7 @@ module StateMachines
|
|
|
355
355
|
# module that gets included in every Object. As a result, state_machine will
|
|
356
356
|
# generate the following warning:
|
|
357
357
|
#
|
|
358
|
-
# Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
|
|
358
|
+
# Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true. Defining :state state machine on Vehicle.
|
|
359
359
|
#
|
|
360
360
|
# Even though you may not be using Kernel's implementation of the "open"
|
|
361
361
|
# instance method, state_machine isn't aware of this and, as a result, stays
|
|
@@ -517,8 +517,7 @@ module StateMachines
|
|
|
517
517
|
|
|
518
518
|
if block_given?
|
|
519
519
|
if !self.class.ignore_method_conflicts && (conflicting_ancestor = owner_class_ancestor_has_method?(scope, method))
|
|
520
|
-
|
|
521
|
-
warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
|
|
520
|
+
warn method_conflict_message(scope, method, conflicting_ancestor)
|
|
522
521
|
else
|
|
523
522
|
name = self.name
|
|
524
523
|
helper_module.class_eval do
|
data/lib/state_machines/state.rb
CHANGED
|
@@ -289,8 +289,8 @@ module StateMachines
|
|
|
289
289
|
def add_predicate
|
|
290
290
|
predicate_method = "#{qualified_name}?"
|
|
291
291
|
|
|
292
|
-
if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
|
|
293
|
-
warn_about_method_conflict(predicate_method,
|
|
292
|
+
if (conflicting_ancestor = machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method))
|
|
293
|
+
warn_about_method_conflict(predicate_method, conflicting_ancestor)
|
|
294
294
|
elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
|
|
295
295
|
warn_about_method_conflict(predicate_method, machine.owner_class)
|
|
296
296
|
else
|
|
@@ -308,7 +308,7 @@ module StateMachines
|
|
|
308
308
|
def warn_about_method_conflict(method, defined_in)
|
|
309
309
|
return if StateMachines::Machine.ignore_method_conflicts
|
|
310
310
|
|
|
311
|
-
warn
|
|
311
|
+
warn machine.send(:method_conflict_message, :instance, method, defined_in)
|
|
312
312
|
end
|
|
313
313
|
end
|
|
314
314
|
end
|
|
@@ -346,6 +346,19 @@ module StateMachines
|
|
|
346
346
|
!halted
|
|
347
347
|
end
|
|
348
348
|
|
|
349
|
+
# Completes a transition whose before phase has already run by executing
|
|
350
|
+
# its after callbacks, resuming any paused around callbacks first. Used
|
|
351
|
+
# to finish transitions that were stored for deferred completion (e.g.
|
|
352
|
+
# generated mid-action from an event attribute) once their action has
|
|
353
|
+
# succeeded.
|
|
354
|
+
def complete_deferred_after_callbacks
|
|
355
|
+
if paused?
|
|
356
|
+
run_callbacks(after: true)
|
|
357
|
+
else
|
|
358
|
+
after
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
349
362
|
private
|
|
350
363
|
|
|
351
364
|
# Runs a block that may get paused. If the block doesn't pause, then
|
|
@@ -394,9 +407,7 @@ module StateMachines
|
|
|
394
407
|
else
|
|
395
408
|
# Capture current fiber's Thread.current storage to preserve object identity
|
|
396
409
|
# This is needed for compatibility but has limitations with dynamic assignments
|
|
397
|
-
parent_fiber_locals =
|
|
398
|
-
storage[key] = Thread.current[key]
|
|
399
|
-
end
|
|
410
|
+
parent_fiber_locals = capture_thread_locals
|
|
400
411
|
|
|
401
412
|
# Create a new fiber to run the block
|
|
402
413
|
fiber = Fiber.new do
|
|
@@ -415,11 +426,7 @@ module StateMachines
|
|
|
415
426
|
end
|
|
416
427
|
|
|
417
428
|
# Export the final thread storage state along with the result
|
|
418
|
-
|
|
419
|
-
storage[key] = Thread.current[key]
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
[halted ? :halted : :completed, thread_storage]
|
|
429
|
+
[halted ? :halted : :completed, capture_thread_locals]
|
|
423
430
|
rescue StandardError => e
|
|
424
431
|
# Store the exception for re-raising
|
|
425
432
|
[:error, e]
|
|
@@ -446,6 +453,14 @@ module StateMachines
|
|
|
446
453
|
end
|
|
447
454
|
end
|
|
448
455
|
|
|
456
|
+
# Snapshots the current fiber's Thread.current storage, preserving object
|
|
457
|
+
# identity of the stored values
|
|
458
|
+
def capture_thread_locals
|
|
459
|
+
Thread.current.keys.each_with_object({}) do |key, storage|
|
|
460
|
+
storage[key] = Thread.current[key]
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
449
464
|
# Unwraps a result returned from a pausable fiber, re-raising any exception
|
|
450
465
|
# captured inside the fiber and importing its exported thread storage
|
|
451
466
|
def unwrap_fiber_result(result)
|
|
@@ -257,11 +257,38 @@ module StateMachines
|
|
|
257
257
|
object.instance_variable_set(:@_state_machine_event_transitions, transitions_by_machine)
|
|
258
258
|
end
|
|
259
259
|
end
|
|
260
|
+
|
|
261
|
+
# Complete any transitions that other machines generated mid-action
|
|
262
|
+
# (e.g. an event attribute set in a before callback and picked up by
|
|
263
|
+
# the validation cycle) so their after callbacks run in this same
|
|
264
|
+
# action cycle instead of leaking into the next one (issue #91)
|
|
265
|
+
complete_nested_transitions unless skip_after
|
|
260
266
|
else
|
|
261
267
|
super
|
|
262
268
|
end
|
|
263
269
|
end
|
|
264
270
|
|
|
271
|
+
# Completes transitions that were stored for deferred completion by
|
|
272
|
+
# machines outside this collection while the action was running. Their
|
|
273
|
+
# before callbacks and state persistence already happened mid-action;
|
|
274
|
+
# only their after callbacks remain. Clears the stored references so
|
|
275
|
+
# they cannot leak into a later action cycle with stale data (issue #91).
|
|
276
|
+
def complete_nested_transitions
|
|
277
|
+
return if empty? || !success?
|
|
278
|
+
|
|
279
|
+
pending = object.instance_variable_get(:@_state_machine_event_transitions)
|
|
280
|
+
return unless pending
|
|
281
|
+
|
|
282
|
+
machines = map(&:machine)
|
|
283
|
+
pending.each_value do |transition|
|
|
284
|
+
next if machines.include?(transition.machine)
|
|
285
|
+
|
|
286
|
+
transition.machine.write(object, :event_transition, nil)
|
|
287
|
+
transition.complete_deferred_after_callbacks
|
|
288
|
+
end
|
|
289
|
+
object.instance_variable_set(:@_state_machine_event_transitions, nil)
|
|
290
|
+
end
|
|
291
|
+
|
|
265
292
|
# Tracks that before callbacks have now completed
|
|
266
293
|
def persist
|
|
267
294
|
@before_run = true
|