ultra-smart-sys 0.0.1

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/state_machines-0.201.0/LICENSE.txt +23 -0
  3. data/state_machines-0.201.0/README.md +1024 -0
  4. data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
  5. data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
  6. data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
  7. data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
  8. data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
  9. data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
  10. data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
  11. data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
  12. data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
  13. data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  14. data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
  15. data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
  16. data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
  17. data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
  18. data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
  19. data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
  20. data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
  21. data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
  22. data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
  23. data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
  24. data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
  25. data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
  26. data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
  27. data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
  28. data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
  29. data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
  30. data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
  31. data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
  32. data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
  33. data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
  34. data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
  35. data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
  36. data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
  37. data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
  38. data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
  39. data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
  40. data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
  41. data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
  42. data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
  43. data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
  44. data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
  45. data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
  46. data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
  47. data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
  48. data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
  49. data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
  50. data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
  51. data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
  52. data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
  53. data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
  54. data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
  55. data/state_machines-0.201.0/lib/state_machines.rb +6 -0
  56. data/ultra-smart-sys.gemspec +12 -0
  57. metadata +96 -0
@@ -0,0 +1,1024 @@
1
+ ![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg)
2
+
3
+ # State Machines
4
+
5
+ State Machines adds support for creating state machines for attributes on any Ruby class.
6
+
7
+ *Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).* If you want to save state in your database, **you need one of these additional integrations**.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'state_machines'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```sh
20
+ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```sh
26
+ gem install state_machines
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Example
32
+
33
+ Below is an example of many of the features offered by this plugin, including:
34
+
35
+ * Initial states
36
+ * Namespaced states
37
+ * Transition callbacks
38
+ * Conditional transitions
39
+ * Coordinated state management guards
40
+ * Asynchronous state machines (async: true)
41
+ * State-driven instance behavior
42
+ * Customized state values
43
+ * Parallel events
44
+ * Path analysis
45
+
46
+ Class definition:
47
+
48
+ ```ruby
49
+ class Vehicle
50
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy, :parking_meter_number
51
+
52
+ state_machine :state, initial: :parked do
53
+ before_transition parked: any - :parked, do: :put_on_seatbelt
54
+
55
+ after_transition on: :crash, do: :tow
56
+ after_transition on: :repair, do: :fix
57
+ after_transition any => :parked do |vehicle, transition|
58
+ vehicle.seatbelt_on = false
59
+ end
60
+
61
+ after_failure on: :ignite, do: :log_start_failure
62
+
63
+ around_transition do |vehicle, transition, block|
64
+ start = Time.now
65
+ block.call
66
+ vehicle.time_used += Time.now - start
67
+ end
68
+
69
+ event :park do
70
+ transition [:idling, :first_gear] => :parked
71
+ end
72
+
73
+ before_transition on: :park do |vehicle, transition|
74
+ # If using Rails:
75
+ # options = transition.args.extract_options!
76
+
77
+ options = transition.args.last.is_a?(Hash) ? transition.args.pop : {}
78
+ meter_number = options[:meter_number]
79
+
80
+ unless meter_number.nil?
81
+ vehicle.parking_meter_number = meter_number
82
+ end
83
+ end
84
+
85
+ event :ignite do
86
+ transition stalled: same, parked: :idling
87
+ end
88
+
89
+ event :idle do
90
+ transition first_gear: :idling
91
+ end
92
+
93
+ event :shift_up do
94
+ transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear
95
+ end
96
+
97
+ event :shift_down do
98
+ transition third_gear: :second_gear, second_gear: :first_gear
99
+ end
100
+
101
+ event :crash do
102
+ transition all - [:parked, :stalled] => :stalled, if: ->(vehicle) {!vehicle.passed_inspection?}
103
+ end
104
+
105
+ event :repair do
106
+ # The first transition that matches the state and passes its conditions
107
+ # will be used
108
+ transition stalled: :parked, unless: :auto_shop_busy
109
+ transition stalled: same
110
+ end
111
+
112
+ state :parked do
113
+ def speed
114
+ 0
115
+ end
116
+ end
117
+
118
+ state :idling, :first_gear do
119
+ def speed
120
+ 10
121
+ end
122
+ end
123
+
124
+ state all - [:parked, :stalled, :idling] do
125
+ def moving?
126
+ true
127
+ end
128
+ end
129
+
130
+ state :parked, :stalled, :idling do
131
+ def moving?
132
+ false
133
+ end
134
+ end
135
+ end
136
+
137
+ state_machine :alarm_state, initial: :active, namespace: :'alarm' do
138
+ event :enable do
139
+ transition all => :active
140
+ end
141
+
142
+ event :disable do
143
+ transition all => :off
144
+ end
145
+
146
+ state :active, :value => 1
147
+ state :off, :value => 0
148
+ end
149
+
150
+ def initialize
151
+ @seatbelt_on = false
152
+ @time_used = 0
153
+ @auto_shop_busy = true
154
+ @parking_meter_number = nil
155
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
156
+ end
157
+
158
+ def put_on_seatbelt
159
+ @seatbelt_on = true
160
+ end
161
+
162
+ def passed_inspection?
163
+ false
164
+ end
165
+
166
+ def tow
167
+ # tow the vehicle
168
+ end
169
+
170
+ def fix
171
+ # get the vehicle fixed by a mechanic
172
+ end
173
+
174
+ def log_start_failure
175
+ # log a failed attempt to start the vehicle
176
+ end
177
+ end
178
+ ```
179
+
180
+ **Note** the comment made on the `initialize` method in the class. In order for
181
+ state machine attributes to be properly initialized, `super()` must be called.
182
+ See `StateMachines:MacroMethods` for more information about this.
183
+
184
+ Using the above class as an example, you can interact with the state machine
185
+ like so:
186
+
187
+ ```ruby
188
+ vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
189
+ vehicle.state # => "parked"
190
+ vehicle.state_name # => :parked
191
+ vehicle.human_state_name # => "parked"
192
+ vehicle.parked? # => true
193
+ vehicle.can_ignite? # => true
194
+ vehicle.ignite_transition # => #<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
195
+ vehicle.state_events # => [:ignite]
196
+ vehicle.state_transitions # => [#<StateMachines:Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
197
+ vehicle.speed # => 0
198
+ vehicle.moving? # => false
199
+
200
+ vehicle.ignite # => true
201
+ vehicle.parked? # => false
202
+ vehicle.idling? # => true
203
+ vehicle.speed # => 10
204
+ vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
205
+
206
+ vehicle.shift_up # => true
207
+ vehicle.speed # => 10
208
+ vehicle.moving? # => true
209
+ vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
210
+
211
+ # A generic event helper is available to fire without going through the event's instance method
212
+ vehicle.fire_state_event(:shift_up) # => true
213
+
214
+ # Call state-driven behavior that's undefined for the state raises a NoMethodError
215
+ vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
216
+ vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
217
+
218
+ # The bang (!) operator can raise exceptions if the event fails
219
+ vehicle.park! # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear
220
+
221
+ # Generic state predicates can raise exceptions if the value does not exist
222
+ vehicle.state?(:parked) # => false
223
+ vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
224
+
225
+ # Transition callbacks can receive arguments
226
+ vehicle.park(meter_number: '12345') # => true
227
+ vehicle.parked? # => true
228
+ vehicle.parking_meter_number # => "12345"
229
+
230
+ # Namespaced machines have uniquely-generated methods
231
+ vehicle.alarm_state # => 1
232
+ vehicle.alarm_state_name # => :active
233
+
234
+ vehicle.can_disable_alarm? # => true
235
+ vehicle.disable_alarm # => true
236
+ vehicle.alarm_state # => 0
237
+ vehicle.alarm_state_name # => :off
238
+ vehicle.can_enable_alarm? # => true
239
+
240
+ vehicle.alarm_off? # => true
241
+ vehicle.alarm_active? # => false
242
+
243
+ # Events can be fired in parallel
244
+ vehicle.fire_events(:shift_down, :enable_alarm) # => true
245
+ vehicle.state_name # => :first_gear
246
+ vehicle.alarm_state_name # => :active
247
+
248
+ vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
249
+
250
+ # Coordinated State Management
251
+
252
+ State machines can coordinate with each other using state guards, allowing transitions to depend on the state of other state machines within the same object. This enables complex system modeling where components are interdependent.
253
+
254
+ ## State Guard Options
255
+
256
+ ### Single State Guards
257
+
258
+ * `:if_state` - Transition only if another state machine is in a specific state.
259
+ * `:unless_state` - Transition only if another state machine is NOT in a specific state.
260
+
261
+ ```ruby
262
+ class TorpedoSystem
263
+ state_machine :bay_doors, initial: :closed do
264
+ event :open do
265
+ transition closed: :open
266
+ end
267
+
268
+ event :close do
269
+ transition open: :closed
270
+ end
271
+ end
272
+
273
+ state_machine :torpedo_status, initial: :loaded do
274
+ event :fire_torpedo do
275
+ # Can only fire torpedo if bay doors are open
276
+ transition loaded: :fired, if_state: { bay_doors: :open }
277
+ end
278
+
279
+ event :reload do
280
+ # Can only reload if bay doors are closed (for safety)
281
+ transition fired: :loaded, unless_state: { bay_doors: :open }
282
+ end
283
+ end
284
+ end
285
+
286
+ system = TorpedoSystem.new
287
+ system.fire_torpedo # => false (bay doors are closed)
288
+
289
+ system.open_bay_doors!
290
+ system.fire_torpedo # => true (bay doors are now open)
291
+ ```
292
+
293
+ ### Multiple State Guards
294
+
295
+ * `:if_all_states` - Transition only if ALL specified state machines are in their respective states.
296
+ * `:unless_all_states` - Transition only if NOT ALL specified state machines are in their respective states.
297
+ * `:if_any_state` - Transition only if ANY of the specified state machines are in their respective states.
298
+ * `:unless_any_state` - Transition only if NONE of the specified state machines are in their respective states.
299
+
300
+ ```ruby
301
+ class StarshipBridge
302
+ state_machine :shields, initial: :down
303
+ state_machine :weapons, initial: :offline
304
+ state_machine :warp_core, initial: :stable
305
+
306
+ state_machine :alert_status, initial: :green do
307
+ event :red_alert do
308
+ # Red alert if ANY critical system needs attention
309
+ transition green: :red, if_any_state: { warp_core: :critical, shields: :down }
310
+ end
311
+
312
+ event :battle_stations do
313
+ # Battle stations only if ALL combat systems are ready
314
+ transition green: :battle, if_all_states: { shields: :up, weapons: :armed }
315
+ end
316
+ end
317
+ end
318
+ ```
319
+
320
+ ## Error Handling
321
+
322
+ State guards provide comprehensive error checking:
323
+
324
+ ```ruby
325
+ # Referencing a non-existent state machine
326
+ event :invalid, if_state: { nonexistent_machine: :some_state }
327
+ # => ArgumentError: State machine 'nonexistent_machine' is not defined for StarshipBridge
328
+
329
+ # Referencing a non-existent state
330
+ event :another_invalid, if_state: { shields: :nonexistent_state }
331
+ # => ArgumentError: State 'nonexistent_state' is not defined in state machine 'shields'
332
+ ```
333
+
334
+ # Asynchronous State Machines
335
+
336
+ State machines can operate asynchronously for high-performance applications. This is ideal for I/O-bound tasks, such as in web servers or other concurrent environments, where you don't want a long-running state transition (like one involving a network call) to block the entire thread.
337
+
338
+ This feature is powered by the [async](https://github.com/socketry/async) gem and uses `concurrent-ruby` for enterprise-grade thread safety.
339
+
340
+ ## Platform Compatibility
341
+
342
+ **Supported Platforms:**
343
+ * MRI Ruby (CRuby) 3.2+
344
+ * Other Ruby engines with full Fiber scheduler support
345
+
346
+ **Unsupported Platforms:**
347
+ * JRuby - Falls back to synchronous mode with warnings
348
+ * TruffleRuby - Falls back to synchronous mode with warnings
349
+
350
+ ## Basic Async Usage
351
+
352
+ Enable async mode by adding `async: true` to your state machine declaration:
353
+
354
+ ```ruby
355
+ class AutonomousDrone < StarfleetShip
356
+ # Async-enabled state machine for autonomous operation
357
+ state_machine :status, async: true, initial: :docked do
358
+ event :launch do
359
+ transition docked: :flying
360
+ end
361
+
362
+ event :land do
363
+ transition flying: :docked
364
+ end
365
+ end
366
+
367
+ # Mixed configuration: some machines async, others sync
368
+ state_machine :teleporter_status, async: true, initial: :offline do
369
+ event :power_up do
370
+ transition offline: :charging
371
+ end
372
+
373
+ event :teleport do
374
+ transition ready: :teleporting
375
+ end
376
+ end
377
+
378
+ # Weapons remain synchronous for safety
379
+ state_machine :weapons, initial: :disarmed do
380
+ event :arm do
381
+ transition disarmed: :armed
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ ## Async Event Methods
388
+
389
+ Async-enabled machines automatically generate async versions of event methods:
390
+
391
+ ```ruby
392
+ drone = AutonomousDrone.new
393
+
394
+ # Within an Async context
395
+ Async do
396
+ # Async event firing - returns Async::Task
397
+ task = drone.launch_async
398
+ result = task.wait # => true
399
+
400
+ # Bang methods for strict error handling
401
+ drone.power_up_async! # => Async::Task (raises on failure)
402
+
403
+ # Generic async event firing
404
+ drone.fire_event_async(:teleport) # => Async::Task
405
+ end
406
+
407
+ # Outside Async context - raises error
408
+ drone.launch_async # => RuntimeError: launch_async must be called within an Async context
409
+ ```
410
+
411
+ ## Thread Safety
412
+
413
+ Async state machines use enterprise-grade thread safety with `concurrent-ruby`:
414
+
415
+ ```ruby
416
+ # Concurrent operations are automatically thread-safe
417
+ threads = []
418
+ 10.times do
419
+ threads << Thread.new do
420
+ Async do
421
+ drone.launch_async.wait
422
+ drone.land_async.wait
423
+ end
424
+ end
425
+ end
426
+ threads.each(&:join)
427
+ ```
428
+
429
+ ## Performance Considerations
430
+
431
+ * **Thread Safety**: Uses `Concurrent::ReentrantReadWriteLock` for optimal read/write performance.
432
+ * **Memory**: Each async-enabled object gets its own mutex (lazy-loaded).
433
+ * **Marshalling**: Objects with async state machines can be serialized (mutex excluded/recreated).
434
+ * **Mixed Mode**: You can mix async and sync state machines in the same class.
435
+
436
+ ## Dependencies
437
+
438
+ Async functionality requires:
439
+
440
+ ```ruby
441
+ # Gemfile (automatically scoped to MRI Ruby)
442
+ platform :ruby do
443
+ gem 'async', '>= 2.25.0'
444
+ gem 'concurrent-ruby', '>= 1.3.5'
445
+ end
446
+ ```
447
+
448
+ *Note: These gems are only installed on supported platforms. JRuby/TruffleRuby won't attempt installation.*
449
+
450
+ # Human-friendly names can be accessed for states/events
451
+ Vehicle.human_state_name(:first_gear) # => "first gear"
452
+ Vehicle.human_alarm_state_name(:active) # => "active"
453
+
454
+ Vehicle.human_state_event_name(:shift_down) # => "shift down"
455
+ Vehicle.human_alarm_state_event_name(:enable) # => "enable"
456
+
457
+ # States / events can also be references by the string version of their name
458
+ Vehicle.human_state_name('first_gear') # => "first gear"
459
+ Vehicle.human_state_event_name('shift_down') # => "shift down"
460
+
461
+ # Available transition paths can be analyzed for an object
462
+ vehicle.state_paths # => [[#<StateMachines:Transition ...], [#<StateMachines:Transition ...], ...]
463
+ vehicle.state_paths.to_states # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
464
+ vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]
465
+
466
+ # Possible states can be analyzed for a class
467
+ Vehicle.state_machine.states.to_a # [#<StateMachines::State name=:parked value="parked" initial=true>, #<StateMachines::State name=:idling value="idling" initial=false>, ...]
468
+ Vehicle.state_machines[:state].states.to_a # [#<StateMachines::State name=:parked value="parked" initial=true>, #<StateMachines::State name=:idling value="idling" initial=false>, ...]
469
+
470
+ # Find all paths that start and end on certain states
471
+ vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
472
+ # #<StateMachines:Transition attribute=:state event=:ignite from="parked" ...>,
473
+ # #<StateMachines:Transition attribute=:state event=:shift_up from="idling" ...>
474
+ # ]]
475
+ # Skipping state_machine and writing to attributes directly
476
+ vehicle.state = "parked"
477
+ vehicle.state # => "parked"
478
+ vehicle.state_name # => :parked
479
+
480
+ # *Note* that the following is not supported (see StateMachines:MacroMethods#state_machine):
481
+ # vehicle.state = :parked
482
+ ```
483
+
484
+ ## Testing
485
+
486
+ State Machines provides an optional `TestHelper` module with assertion methods to make testing state machines easier and more expressive.
487
+
488
+ **Note: TestHelper is not required by default** - you must explicitly require it in your test files.
489
+
490
+ ### Setup
491
+
492
+ First, require the test helper module, then include it in your test class:
493
+
494
+ ```ruby
495
+ # For Minitest
496
+ require 'state_machines/test_helper'
497
+
498
+ class VehicleTest < Minitest::Test
499
+ include StateMachines::TestHelper
500
+
501
+ def test_initial_state
502
+ vehicle = Vehicle.new
503
+ assert_sm_state vehicle, :parked
504
+ end
505
+ end
506
+
507
+ # For RSpec
508
+ require 'state_machines/test_helper'
509
+
510
+ RSpec.describe Vehicle do
511
+ include StateMachines::TestHelper
512
+
513
+ it "starts in parked state" do
514
+ vehicle = Vehicle.new
515
+ assert_sm_state vehicle, :parked
516
+ end
517
+ end
518
+ ```
519
+
520
+ ### Available Assertions
521
+
522
+ The TestHelper provides both basic assertions and comprehensive state machine-specific assertions with `sm_` prefixes:
523
+
524
+ #### Basic Assertions
525
+
526
+ ```ruby
527
+ vehicle = Vehicle.new
528
+
529
+ # New standardized API (all methods prefixed with assert_sm_)
530
+ assert_sm_state(vehicle, :parked) # Uses default :state machine
531
+ assert_sm_state(vehicle, :parked, machine_name: :status) # Specify machine explicitly
532
+ assert_sm_can_transition(vehicle, :ignite) # Test transition capability
533
+ assert_sm_cannot_transition(vehicle, :shift_up) # Test transition restriction
534
+ assert_sm_transition(vehicle, :ignite, :idling) # Test actual transition
535
+
536
+ # Multi-FSM examples
537
+ assert_sm_state(vehicle, :inactive, machine_name: :insurance_state) # Test insurance state
538
+ assert_sm_can_transition(vehicle, :buy_insurance, machine_name: :insurance_state)
539
+ ```
540
+
541
+ #### Extended State Machine Assertions
542
+
543
+ ```ruby
544
+ machine = Vehicle.state_machine(:state)
545
+ vehicle = Vehicle.new
546
+
547
+ # State configuration
548
+ assert_sm_states_list machine, [:parked, :idling, :stalled]
549
+ assert_sm_initial_state machine, :parked
550
+
551
+ # Event behavior
552
+ assert_sm_event_triggers vehicle, :ignite
553
+ refute_sm_event_triggers vehicle, :shift_up
554
+ assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTransition
555
+
556
+ # Persistence (with ActiveRecord integration)
557
+ assert_sm_state_persisted record, expected: :active
558
+ ```
559
+
560
+ #### Indirect Event Testing
561
+
562
+ Test that methods trigger state machine events indirectly:
563
+
564
+ ```ruby
565
+ # Minitest style
566
+ vehicle = Vehicle.new
567
+ vehicle.ignite # Put in idling state
568
+
569
+ # Test that a custom method triggers a specific event
570
+ assert_sm_triggers_event(vehicle, :crash) do
571
+ vehicle.redline # Custom method that calls crash! internally
572
+ end
573
+
574
+ # Test multiple events
575
+ assert_sm_triggers_event(vehicle, [:crash, :emergency]) do
576
+ vehicle.emergency_stop
577
+ end
578
+
579
+ # Test on specific state machine (multi-FSM support)
580
+ assert_sm_triggers_event(vehicle, :disable, machine_name: :alarm) do
581
+ vehicle.turn_off_alarm
582
+ end
583
+ ```
584
+
585
+ ```ruby
586
+ # RSpec style (coming soon with proper matcher support)
587
+ RSpec.describe Vehicle do
588
+ include StateMachines::TestHelper
589
+
590
+ it "triggers crash when redlining" do
591
+ vehicle = Vehicle.new
592
+ vehicle.ignite
593
+
594
+ expect_to_trigger_event(vehicle, :crash) do
595
+ vehicle.redline
596
+ end
597
+ end
598
+ end
599
+ ```
600
+
601
+ #### Callback Definition Testing (TDD Support)
602
+
603
+ Verify that callbacks are properly defined in your state machine:
604
+
605
+ ```ruby
606
+ # Test after_transition callbacks
607
+ assert_after_transition(Vehicle, on: :crash, do: :tow)
608
+ assert_after_transition(Vehicle, from: :stalled, to: :parked, do: :log_repair)
609
+
610
+ # Test before_transition callbacks
611
+ assert_before_transition(Vehicle, from: :parked, do: :put_on_seatbelt)
612
+ assert_before_transition(Vehicle, on: :ignite, if: :seatbelt_on?)
613
+
614
+ # Works with machine instances too
615
+ machine = Vehicle.state_machine(:state)
616
+ assert_after_transition(machine, on: :crash, do: :tow)
617
+ ```
618
+
619
+ #### Multiple State Machine Support
620
+
621
+ The TestHelper fully supports objects with multiple state machines:
622
+
623
+ ```ruby
624
+ # Example: StarfleetShip with 3 state machines
625
+ ship = StarfleetShip.new
626
+
627
+ # Test states on different machines
628
+ assert_sm_state(ship, :docked, machine_name: :status) # Main ship status
629
+ assert_sm_state(ship, :down, machine_name: :shields) # Shield system
630
+ assert_sm_state(ship, :standby, machine_name: :weapons) # Weapons system
631
+
632
+ # Test transitions on specific machines
633
+ assert_sm_transition(ship, :undock, :impulse, machine_name: :status)
634
+ assert_sm_transition(ship, :raise_shields, :up, machine_name: :shields)
635
+ assert_sm_transition(ship, :arm_weapons, :armed, machine_name: :weapons)
636
+
637
+ # Test event triggering across multiple machines
638
+ assert_sm_triggers_event(ship, :red_alert, machine_name: :status) do
639
+ ship.engage_combat_mode # Custom method affecting multiple systems
640
+ end
641
+
642
+ assert_sm_triggers_event(ship, :raise_shields, machine_name: :shields) do
643
+ ship.engage_combat_mode
644
+ end
645
+
646
+ # Test callback definitions on specific machines
647
+ shields_machine = StarfleetShip.state_machine(:shields)
648
+ assert_before_transition(shields_machine, from: :down, to: :up, do: :power_up_shields)
649
+
650
+ # Test persistence across multiple machines
651
+ assert_sm_state_persisted(ship, "impulse", :status)
652
+ assert_sm_state_persisted(ship, "up", :shields)
653
+ assert_sm_state_persisted(ship, "armed", :weapons)
654
+ ```
655
+
656
+ The test helper works with both Minitest and RSpec, automatically detecting your testing framework.
657
+
658
+ **Note:** All methods use consistent keyword arguments with `machine_name:` as the last parameter, making the API intuitive and Grep-friendly.
659
+
660
+ ## Additional Topics
661
+
662
+ ### Explicit vs. Implicit Event Transitions
663
+
664
+ Every event defined for a state machine generates an instance method on the
665
+ class that allows the event to be explicitly triggered. Most of the examples in
666
+ the state_machine documentation use this technique. However, with some types of
667
+ integrations, like ActiveRecord, you can also *implicitly* fire events by
668
+ setting a special attribute on the instance.
669
+
670
+ Suppose you're using the ActiveRecord integration and the following model is
671
+ defined:
672
+
673
+ ```ruby
674
+ class Vehicle < ActiveRecord::Base
675
+ state_machine initial: :parked do
676
+ event :ignite do
677
+ transition parked: :idling
678
+ end
679
+ end
680
+ end
681
+ ```
682
+
683
+ To trigger the `ignite` event, you would typically call the `Vehicle#ignite`
684
+ method like so:
685
+
686
+ ```ruby
687
+ vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
688
+ vehicle.ignite # => true
689
+ vehicle.state # => "idling"
690
+ ```
691
+
692
+ This is referred to as an *explicit* event transition. The same behavior can
693
+ also be achieved *implicitly* by setting the state event attribute and invoking
694
+ the action associated with the state machine. For example:
695
+
696
+ ```ruby
697
+ vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
698
+ vehicle.state_event = 'ignite' # => 'ignite'
699
+ vehicle.save # => true
700
+ vehicle.state # => 'idling'
701
+ vehicle.state_event # => nil
702
+ ```
703
+
704
+ As you can see, the `ignite` event was automatically triggered when the `save`
705
+ action was called. This is particularly useful if you want to allow users to
706
+ drive the state transitions from a web API.
707
+
708
+ See each integration's API documentation for more information on the implicit
709
+ approach.
710
+
711
+ ### Symbols vs. Strings
712
+
713
+ In all of the examples used throughout the documentation, you'll notice that
714
+ states and events are almost always referenced as symbols. This isn't a
715
+ requirement, but rather a suggested best practice.
716
+
717
+ You can very well define your state machine with Strings like so:
718
+
719
+ ```ruby
720
+ class Vehicle
721
+ state_machine initial: 'parked' do
722
+ event 'ignite' do
723
+ transition 'parked' => 'idling'
724
+ end
725
+
726
+ # ...
727
+ end
728
+ end
729
+ ```
730
+
731
+ You could even use numbers as your state / event names. The **important** thing
732
+ to keep in mind is that the type being used for referencing states / events in
733
+ your machine definition must be **consistent**. If you're using Symbols, then
734
+ all states / events must use Symbols. Otherwise you'll encounter the following
735
+ error:
736
+
737
+ ```ruby
738
+ class Vehicle
739
+ state_machine do
740
+ event :ignite do
741
+ transition parked: 'idling'
742
+ end
743
+ end
744
+ end
745
+
746
+ # => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent
747
+ ```
748
+
749
+ There **is** an exception to this rule. The consistency is only required within
750
+ the definition itself. However, when the machine's helper methods are called
751
+ with input from external sources, such as a web form, state_machine will map
752
+ that input to a String / Symbol. For example:
753
+
754
+ ```ruby
755
+ class Vehicle
756
+ state_machine initial: :parked do
757
+ event :ignite do
758
+ transition parked: :idling
759
+ end
760
+ end
761
+ end
762
+
763
+ v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state="parked">
764
+ v.state?('parked') # => true
765
+ v.state?(:parked) # => true
766
+ ```
767
+
768
+ **Note** that none of this actually has to do with the type of the value that
769
+ gets stored. By default, all state values are assumed to be string -- regardless
770
+ of whether the state names are symbols or strings. If you want to store states
771
+ as symbols instead you'll have to be explicit about it:
772
+
773
+ ```ruby
774
+ class Vehicle
775
+ state_machine initial: :parked do
776
+ event :ignite do
777
+ transition parked: :idling
778
+ end
779
+
780
+ states.each do |state|
781
+ self.state(state.name, :value => state.name.to_sym)
782
+ end
783
+ end
784
+ end
785
+
786
+ v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state=:parked>
787
+ v.state?('parked') # => true
788
+ v.state?(:parked) # => true
789
+ ```
790
+
791
+ ### Syntax flexibility
792
+
793
+ Although state_machine introduces a simplified syntax, it still remains
794
+ backwards compatible with previous versions and other state-related libraries by
795
+ providing some flexibility around how transitions are defined. See below for an
796
+ overview of these syntaxes.
797
+
798
+ #### Verbose syntax
799
+
800
+ In general, it's recommended that state machines use the implicit syntax for
801
+ transitions. However, you can be a little more explicit and verbose about
802
+ transitions by using the `:from`, `:except_from`, `:to`,
803
+ and `:except_to` options.
804
+
805
+ For example, transitions and callbacks can be defined like so:
806
+
807
+ ```ruby
808
+ class Vehicle
809
+ state_machine initial: :parked do
810
+ before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
811
+ after_transition to: :parked do |vehicle, transition|
812
+ vehicle.seatbelt = 'off'
813
+ end
814
+
815
+ event :ignite do
816
+ transition from: :parked, to: :idling
817
+ end
818
+ end
819
+ end
820
+ ```
821
+
822
+ #### Transition context
823
+
824
+ Some flexibility is provided around the context in which transitions can be
825
+ defined. In almost all examples throughout the documentation, transitions are
826
+ defined within the context of an event. If you prefer to have state machines
827
+ defined in the context of a **state** either out of preference or in order to
828
+ easily migrate from a different library, you can do so as shown below:
829
+
830
+ ```ruby
831
+ class Vehicle
832
+ state_machine initial: :parked do
833
+ # ...
834
+
835
+ state :parked do
836
+ transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
837
+
838
+ def speed
839
+ 0
840
+ end
841
+ end
842
+
843
+ state :first_gear do
844
+ transition to: :second_gear, on: :shift_up
845
+
846
+ def speed
847
+ 10
848
+ end
849
+ end
850
+
851
+ state :idling, :first_gear do
852
+ transition to: :parked, on: :park
853
+ end
854
+ end
855
+ end
856
+ ```
857
+
858
+ In the above example, there's no need to specify the `from` state for each
859
+ transition since it's inferred from the context.
860
+
861
+ You can also define transitions completely outside the context of a particular
862
+ state / event. This may be useful in cases where you're building a state
863
+ machine from a data store instead of part of the class definition. See the
864
+ example below:
865
+
866
+ ```ruby
867
+ class Vehicle
868
+ state_machine initial: :parked do
869
+ # ...
870
+
871
+ transition parked: :idling, :on => [:ignite, :shift_up]
872
+ transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
873
+ transition [:idling, :first_gear] => :parked, on: :park
874
+ transition all - [:parked, :stalled]: :stalled, unless: :auto_shop_busy?
875
+ end
876
+ end
877
+ ```
878
+
879
+ Notice that in these alternative syntaxes:
880
+
881
+ * You can continue to configure `:if` and `:unless` conditions
882
+ * You can continue to define `from` states (when in the machine context) using
883
+ the `all`, `any`, and `same` helper methods
884
+
885
+ ### Static / Dynamic definitions
886
+
887
+ In most cases, the definition of a state machine is **static**. That is to say,
888
+ the states, events and possible transitions are known ahead of time even though
889
+ they may depend on data that's only known at runtime. For example, certain
890
+ transitions may only be available depending on an attribute on that object it's
891
+ being run on. All of the documentation in this library define static machines
892
+ like so:
893
+
894
+ ```ruby
895
+ class Vehicle
896
+ state_machine :state, initial: :parked do
897
+ event :park do
898
+ transition [:idling, :first_gear] => :parked
899
+ end
900
+
901
+ # ...
902
+ end
903
+ end
904
+ ```
905
+
906
+ #### Draw state machines
907
+
908
+ State machines includes a default STDIORenderer for debugging state machines without external dependencies.
909
+ This renderer can be used to visualize the state machine in the console.
910
+
911
+ To use the renderer, simply call the `draw` method on the state machine:
912
+
913
+ ```ruby
914
+ Vehicle.state_machine.draw # Outputs the state machine diagram to the console
915
+ ```
916
+
917
+ You can customize the output by passing in options to the `draw` method, such as the output stream:
918
+
919
+ ```ruby
920
+ Vehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr
921
+ ```
922
+
923
+ #### Dynamic definitions
924
+
925
+ There may be cases where the definition of a state machine is **dynamic**.
926
+ This means that you don't know the possible states or events for a machine until
927
+ runtime. For example, you may allow users in your application to manage the
928
+ state machine of a project or task in your system. This means that the list of
929
+ transitions (and their associated states / events) could be stored externally,
930
+ such as in a database. In a case like this, you can define dynamically-generated
931
+ state machines like so:
932
+
933
+ ```ruby
934
+ class Vehicle
935
+ attr_accessor :state
936
+
937
+ # Make sure the machine gets initialized so the initial state gets set properly
938
+ def initialize(*)
939
+ super
940
+ machine
941
+ end
942
+
943
+ # Replace this with an external source (like a db)
944
+ def transitions
945
+ [
946
+ {parked: :idling, on: :ignite},
947
+ {idling: :first_gear, first_gear: :second_gear, on: :shift_up}
948
+ # ...
949
+ ]
950
+ end
951
+
952
+ # Create a state machine for this vehicle instance dynamically based on the
953
+ # transitions defined from the source above
954
+ def machine
955
+ vehicle = self
956
+ @machine ||= Machine.new(vehicle, initial: :parked, action: :save) do
957
+ vehicle.transitions.each {|attrs| transition(attrs)}
958
+ end
959
+ end
960
+
961
+ def save
962
+ # Save the state change...
963
+ true
964
+ end
965
+ end
966
+
967
+ # Generic class for building machines
968
+ class Machine
969
+ def self.new(object, *args, &block)
970
+ machine_class = Class.new
971
+ machine = machine_class.state_machine(*args, &block)
972
+ attribute = machine.attribute
973
+ action = machine.action
974
+
975
+ # Delegate attributes
976
+ machine_class.class_eval do
977
+ define_method(:definition) { machine }
978
+ define_method(attribute) { object.send(attribute) }
979
+ define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) }
980
+ define_method(action) { object.send(action) } if action
981
+ end
982
+
983
+ machine_class.new
984
+ end
985
+ end
986
+
987
+ vehicle = Vehicle.new # => #<Vehicle:0xb708412c @state="parked" ...>
988
+ vehicle.state # => "parked"
989
+ vehicle.machine.ignite # => true
990
+ vehicle.machine.state # => "idling"
991
+ vehicle.state # => "idling"
992
+ vehicle.machine.state_transitions # => [#<StateMachines:Transition ...>]
993
+ vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling
994
+ ```
995
+
996
+ As you can see, state_machine provides enough flexibility for you to be able
997
+ to create new machine definitions on the fly based on an external source of
998
+ transitions.
999
+
1000
+ ## Dependencies
1001
+
1002
+ Ruby versions officially supported and tested:
1003
+
1004
+ * Ruby (MRI) 3.0.0+
1005
+
1006
+ For graphing state machine:
1007
+
1008
+ * [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz)
1009
+
1010
+ For documenting state machines:
1011
+
1012
+ * [state_machines-yard](https://github.com/state-machines/state_machines-yard)
1013
+
1014
+ For RSpec testing, use the custom RSpec matchers:
1015
+
1016
+ * [state_machines-rspec](https://github.com/state-machines/state_machines-rspec)
1017
+
1018
+ ## Contributing
1019
+
1020
+ 1. Fork it ( <https://github.com/state-machines/state_machines/fork> )
1021
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
1022
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
1023
+ 4. Push to the branch (`git push origin my-new-feature`)
1024
+ 5. Create a new Pull Request