state_machines 0.100.2 → 0.101.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: b240916b3b1592fd9980f8dc7583dac5b9af93cf38c193ffb6e2427f3f15f6c0
4
- data.tar.gz: ea38fc11bd6619335415d3a90b92f7f5909df03e5fd087aaf14716dca977d847
3
+ metadata.gz: 0b9c5ce3b64a05bfea23f266688b2dd1623124d030e4d4eaaa31d0c8b9c5f28c
4
+ data.tar.gz: 3126dccbc693deb8b3255408202400360fdffb14e439eda3923f4c3a320f452f
5
5
  SHA512:
6
- metadata.gz: 80517f65fa434f346e552677e08cdd6b04e3d573eef756d058645a4d067bb1dd21331d86d657b0df429cfc3157d48cff3bfe594bb7bcb79f4a34b89cf880150c
7
- data.tar.gz: '01874fcbb634ec7daf80bd3a5d87169644916ebb71e4bbfaaf1891ce53beac963261e2684df288b8aaedf057dae66e64f3167bdd6a676b095562096edbf25c42'
6
+ metadata.gz: d6982ddb9e837d4e7d7763b69045733f2751a74a867fa38789f58e7232a96583f7b2336ed8af7d5ebd0196ae58562aa8f19fb7d2f0f79425a1c5c22e6ddda966
7
+ data.tar.gz: 315a781c1daa8f85a6b9f20790245191f5540014fb35b1e2a2a41827828ab7748b33de43e016b088da3054f9ef60b541ba390d492d7aad23f95f546b9b9e933d
@@ -113,18 +113,10 @@ module StateMachines
113
113
  # Update all states to reflect the new initial state
114
114
  states.each { |state| state.initial = (state.name == @initial_state) }
115
115
 
116
- # Output a warning if there are conflicting initial states for the machine's
117
- # attribute
118
- initial_state = states.detect(&:initial)
119
- has_owner_default = !owner_class_attribute_default.nil?
120
- has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
121
- return unless has_owner_default && has_conflicting_default
122
-
123
- warn(
124
- "Both #{owner_class.name} and its #{name.inspect} machine have defined " \
125
- "a different default for \"#{attribute}\". Use only one or the other for " \
126
- 'defining defaults to avoid unexpected behaviors.'
127
- )
116
+ # Warn if the owner class and the machine have conflicting defaults for
117
+ # the machine's attribute. May be deferred by integrations (e.g.
118
+ # ActiveRecord) to avoid touching the DB at class load time.
119
+ schedule_conflicting_attribute_default_check
128
120
  end
129
121
 
130
122
  # Gets the attribute name for the given machine scope.
@@ -58,6 +58,28 @@ module StateMachines
58
58
  state.matches?(owner_class_attribute_default)
59
59
  end
60
60
 
61
+ # Warns if the owner class and the machine have defined conflicting
62
+ # defaults for the machine's attribute.
63
+ def check_conflicting_attribute_default
64
+ initial_state = states.detect(&:initial)
65
+ has_owner_default = !owner_class_attribute_default.nil?
66
+ has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
67
+ return unless has_owner_default && has_conflicting_default
68
+
69
+ warn(
70
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined " \
71
+ "a different default for \"#{attribute}\". Use only one or the other for " \
72
+ 'defining defaults to avoid unexpected behaviors.'
73
+ )
74
+ end
75
+
76
+ # Schedules or immediately runs the conflicting attribute default check.
77
+ # Override in integrations to defer the check (e.g. until after the DB
78
+ # is ready) to avoid triggering a database connection at class load time.
79
+ def schedule_conflicting_attribute_default_check
80
+ check_conflicting_attribute_default
81
+ end
82
+
61
83
  private
62
84
 
63
85
  # Gets the default messages that can be used in the machine for invalid
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ripper'
4
-
5
3
  module StateMachines
6
4
  # Cross-platform syntax validation for eval strings
7
5
  # Supports CRuby, JRuby, TruffleRuby via pluggable backends
@@ -15,12 +13,12 @@ module StateMachines
15
13
  private
16
14
 
17
15
  # Lazily pick the best backend for this platform
18
- # Prefer RubyVM for performance on CRuby, fallback to Ripper for compatibility
16
+ # Prefer RubyVM for performance on CRuby, fallback to eval for compatibility
19
17
  def backend
20
18
  @backend ||= if RubyVmBackend.available?
21
19
  RubyVmBackend
22
20
  else
23
- RipperBackend
21
+ UniversalBackend
24
22
  end
25
23
  end
26
24
  module_function :backend
@@ -40,16 +38,15 @@ module StateMachines
40
38
  module_function :validate!
41
39
  end
42
40
 
43
- # Universal Ruby backend via Ripper
44
- module RipperBackend
41
+ # Universal Ruby backend
42
+ module UniversalBackend
45
43
  def validate!(code, filename)
46
- sexp = Ripper.sexp(code)
47
- if sexp.nil?
48
- # Ripper.sexp returns nil on a parse error, but no exception
49
- raise SyntaxError, "syntax error in #{filename}"
50
- end
51
-
52
- true
44
+ code = code.b
45
+ code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
46
+ "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
47
+ }
48
+ code = code.force_encoding(Encoding::UTF_8)
49
+ catch { |tag| eval(code, binding, filename, __LINE__ - 1) } == :ok
53
50
  end
54
51
  module_function :validate!
55
52
  end
@@ -44,6 +44,7 @@ module StateMachines
44
44
  @paused_fiber = nil
45
45
  @resuming = false
46
46
  @continuation_block = nil
47
+ @fiber_thread_storage = nil
47
48
 
48
49
  @event = machine.events.fetch(event)
49
50
  @from_state = machine.states.fetch(from_name)
@@ -201,7 +202,8 @@ module StateMachines
201
202
  # this is an idempotent call on an already-paused transition. Just return true.
202
203
  return true if @paused_fiber&.alive? && !options[:after]
203
204
 
204
- # Extract pausable options
205
+ # Always use fibers for compatibility with existing pause/resume functionality
206
+ # The fiber argument can still be used to explicitly control fiber usage
205
207
  pausable_options = options.key?(:fiber) ? { fiber: options[:fiber] } : {}
206
208
 
207
209
  # Check if we're resuming from a pause
@@ -288,6 +290,7 @@ module StateMachines
288
290
  @paused_fiber = nil
289
291
  @resuming = false
290
292
  @continuation_block = nil
293
+ @fiber_thread_storage = nil
291
294
  end
292
295
 
293
296
  # Determines equality of transitions by testing whether the object, states,
@@ -380,27 +383,43 @@ module StateMachines
380
383
  # Handle different result types
381
384
  case result
382
385
  when Array
383
- # Exception occurred inside the fiber
384
386
  if result[0] == :error
385
- # Clean up state before re-raising
387
+ # Exception occurred inside the fiber
386
388
  @paused_fiber = nil
387
389
  raise result[1]
388
- end
389
- else
390
- # Normal flow
391
- # Check if fiber is still alive after resume
392
- if @paused_fiber.alive?
393
- # Still paused, keep the fiber
394
- true
395
390
  else
396
- # Fiber completed
397
- @paused_fiber = nil
398
- result == :halted
391
+ # Normal completion with thread storage export
392
+ @fiber_thread_storage = result[1] if result.length == 2 && result[1].is_a?(Hash)
393
+ result_value = result[0]
399
394
  end
395
+ else
396
+ # Direct result value (paused or simple completion)
397
+ result_value = result
398
+ end
399
+
400
+ # Check if fiber is still alive after resume
401
+ if @paused_fiber.alive?
402
+ # Still paused, keep the fiber
403
+ true
404
+ else
405
+ # Fiber completed
406
+ @paused_fiber = nil
407
+ result_value == :halted
400
408
  end
401
409
  else
410
+ # Capture current fiber's Thread.current storage to preserve object identity
411
+ # This is needed for compatibility but has limitations with dynamic assignments
412
+ parent_fiber_locals = Thread.current.keys.each_with_object({}) do |key, storage|
413
+ storage[key] = Thread.current[key]
414
+ end
415
+
402
416
  # Create a new fiber to run the block
403
417
  fiber = Fiber.new do
418
+ # Restore parent's Thread.current storage with exact same object references
419
+ parent_fiber_locals.each do |key, value|
420
+ Thread.current[key] = value
421
+ end
422
+
404
423
  # Mark that we're inside a pausable fiber
405
424
  Fiber.current.extend(StateMachines::PausableFiber)
406
425
  Fiber.current.state_machine_fiber_pausable = true
@@ -409,7 +428,13 @@ module StateMachines
409
428
  yield
410
429
  true
411
430
  end
412
- halted ? :halted : :completed
431
+
432
+ # Export the final thread storage state along with the result
433
+ thread_storage = Thread.current.keys.each_with_object({}) do |key, storage|
434
+ storage[key] = Thread.current[key]
435
+ end
436
+
437
+ [halted ? :halted : :completed, thread_storage]
413
438
  rescue StandardError => e
414
439
  # Store the exception for re-raising
415
440
  [:error, e]
@@ -425,23 +450,28 @@ module StateMachines
425
450
  # Handle different result types
426
451
  case result
427
452
  when Array
428
- # Exception occurred
429
453
  if result[0] == :error
430
- # Clean up state before re-raising
454
+ # Exception occurred
431
455
  @paused_fiber = nil
432
456
  raise result[1]
433
- end
434
- else
435
- # Normal flow
436
- # Save if paused
437
- if fiber.alive?
438
- @paused_fiber = fiber
439
- # Return true to indicate paused (treated as halted for flow control)
440
- true
441
457
  else
442
- # Fiber completed, return whether it was halted
443
- result == :halted
458
+ # Normal completion - check if we have thread storage
459
+ @fiber_thread_storage = result[1] if result.length == 2 && result[1].is_a?(Hash)
460
+ result_value = result[0]
444
461
  end
462
+ else
463
+ # Direct result value (shouldn't happen with our new code)
464
+ result_value = result
465
+ end
466
+
467
+ # Save if paused
468
+ if fiber.alive?
469
+ @paused_fiber = fiber
470
+ # Return true to indicate paused (treated as halted for flow control)
471
+ true
472
+ else
473
+ # Fiber completed, return whether it was halted
474
+ result_value == :halted
445
475
  end
446
476
  end
447
477
  end
@@ -539,6 +569,10 @@ module StateMachines
539
569
  def after
540
570
  return if @after_run
541
571
 
572
+ # Restore the fiber's thread storage to ensure consistency
573
+ # This preserves Thread.current state from before/around callbacks to after callbacks
574
+ @fiber_thread_storage.each { |key, value| Thread.current[key] = value } if @fiber_thread_storage
575
+
542
576
  catch(:halt) do
543
577
  type = @success ? :after : :failure
544
578
  machine.callbacks[type].each { |callback| callback.call(object, context, self) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StateMachines
4
- VERSION = '0.100.2'
4
+ VERSION = '0.101.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.100.2
4
+ version: 0.101.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -28,16 +28,16 @@ dependencies:
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: '5.4'
33
+ version: 5.27.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: '5.4'
40
+ version: 5.27.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubygems_version: 3.6.9
138
+ rubygems_version: 4.0.3
139
139
  specification_version: 4
140
140
  summary: State machines for attributes
141
141
  test_files: []