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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10897c2b3fdaea1f4728eb063c09f7996c6164f938d814dc486c746c4e91c081
4
- data.tar.gz: e7e89af7bfa089d2f1a7c7324f028223e530231e808e34398a2665c11992c168
3
+ metadata.gz: e579c5afa633f1402bfd1e1a5ebd0c446ff410eb3b06940d74dce0d0e024f530
4
+ data.tar.gz: 629c2f403c71e966e98c5d35c64fa8ffa99caaa8e5aced5e205a569eb867e811
5
5
  SHA512:
6
- metadata.gz: cb901d7e14ae688dbfec57c312a6edc338156dc0b239a9f56f6898443bbb3ed33e1aad85a260b5ab6c6aa21f3513fdac1e19f24b09418d8994be6f6f80a50a41
7
- data.tar.gz: ca11e59b75a28e0c3a9f27fae6a9ac194082a9a1a034d1160050f21c8645f69111e6179e64c5d0048143f5d095f789207372c9d2ef3b3d2a0a07e10cd154438d
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
- # Use the bang version which raises exceptions on failure
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.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
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.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
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
- ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
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
@@ -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, machine.owner_class.ancestors.first)
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 "Instance method #{method.inspect} is already defined in #{defined_in.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
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 = Thread.current.keys.each_with_object({}) do |key, storage|
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
- thread_storage = Thread.current.keys.each_with_object({}) do |key, storage|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StateMachines
4
- VERSION = '0.200.0'
4
+ VERSION = '0.202.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.200.0
4
+ version: 0.202.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih