state_machines 0.40.0 → 0.50.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 +202 -0
- data/lib/state_machines/branch.rb +55 -14
- data/lib/state_machines/event.rb +1 -1
- 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: 247ae1ee6fa7beb6ac29a68dfd400ea41a3923b973d842a5adb2ef78960b3266
|
4
|
+
data.tar.gz: 7240a29d3d87740ade0d0197e9c4c0192176023f23e453d1ba659e8be905afef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26e8c0a197cdc254c4157a57e3bcfabfef87240280555ae623d993e30ccb68ba6433e911da171f60beb0e756af54aa60ff18395c9d49c6719a06b22bd895bdca
|
7
|
+
data.tar.gz: 9dfbdceba5b428f415dfeeba91995ab52640282b2b6c2fb92dea266c97677263331706e6b37f09a6280dc3f578da27dae1f5ade628a655342e46c565ddbe05db
|
data/README.md
CHANGED
@@ -36,6 +36,8 @@ Below is an example of many of the features offered by this plugin, including:
|
|
36
36
|
* Namespaced states
|
37
37
|
* Transition callbacks
|
38
38
|
* Conditional transitions
|
39
|
+
* Coordinated state management guards
|
40
|
+
* Asynchronous state machines (async: true)
|
39
41
|
* State-driven instance behavior
|
40
42
|
* Customized state values
|
41
43
|
* Parallel events
|
@@ -245,6 +247,206 @@ vehicle.alarm_state_name # => :active
|
|
245
247
|
|
246
248
|
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
|
247
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
|
+
|
248
450
|
# Human-friendly names can be accessed for states/events
|
249
451
|
Vehicle.human_state_name(:first_gear) # => "first gear"
|
250
452
|
Vehicle.human_alarm_state_name(:active) # => "active"
|
@@ -34,6 +34,12 @@ module StateMachines
|
|
34
34
|
# Build conditionals
|
35
35
|
@if_condition = options.delete(:if)
|
36
36
|
@unless_condition = options.delete(:unless)
|
37
|
+
@if_state_condition = options.delete(:if_state)
|
38
|
+
@unless_state_condition = options.delete(:unless_state)
|
39
|
+
@if_all_states_condition = options.delete(:if_all_states)
|
40
|
+
@unless_all_states_condition = options.delete(:unless_all_states)
|
41
|
+
@if_any_state_condition = options.delete(:if_any_state)
|
42
|
+
@unless_any_state_condition = options.delete(:unless_any_state)
|
37
43
|
|
38
44
|
# Build event requirement
|
39
45
|
@event_requirement = build_matcher(options, :on, :except_on)
|
@@ -182,21 +188,56 @@ module StateMachines
|
|
182
188
|
# Verifies that the conditionals for this branch evaluate to true for the
|
183
189
|
# given object. Event arguments are passed to guards that accept multiple parameters.
|
184
190
|
def matches_conditions?(object, query, event_args = [])
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
191
|
+
return true if query[:guard] == false
|
192
|
+
|
193
|
+
# Evaluate original if/unless conditions
|
194
|
+
if_passes = !if_condition || Array(if_condition).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
|
195
|
+
unless_passes = !unless_condition || Array(unless_condition).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
|
196
|
+
|
197
|
+
return false unless if_passes && unless_passes
|
198
|
+
|
199
|
+
# Consolidate all state guards
|
200
|
+
state_guards = {
|
201
|
+
if_state: @if_state_condition,
|
202
|
+
unless_state: @unless_state_condition,
|
203
|
+
if_all_states: @if_all_states_condition,
|
204
|
+
unless_all_states: @unless_all_states_condition,
|
205
|
+
if_any_state: @if_any_state_condition,
|
206
|
+
unless_any_state: @unless_any_state_condition
|
207
|
+
}.compact
|
208
|
+
|
209
|
+
return true if state_guards.empty?
|
210
|
+
|
211
|
+
validate_and_check_state_guards(object, state_guards)
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def validate_and_check_state_guards(object, guards)
|
217
|
+
guards.all? do |guard_type, conditions|
|
218
|
+
case guard_type
|
219
|
+
when :if_state, :if_all_states
|
220
|
+
conditions.all? { |machine, state| check_state(object, machine, state) }
|
221
|
+
when :unless_state
|
222
|
+
conditions.none? { |machine, state| check_state(object, machine, state) }
|
223
|
+
when :if_any_state
|
224
|
+
conditions.any? { |machine, state| check_state(object, machine, state) }
|
225
|
+
when :unless_all_states
|
226
|
+
!conditions.all? { |machine, state| check_state(object, machine, state) }
|
227
|
+
when :unless_any_state
|
228
|
+
conditions.none? { |machine, state| check_state(object, machine, state) }
|
229
|
+
end
|
199
230
|
end
|
200
231
|
end
|
232
|
+
|
233
|
+
def check_state(object, machine_name, state_name)
|
234
|
+
machine = object.class.state_machines[machine_name]
|
235
|
+
raise ArgumentError, "State machine '#{machine_name}' is not defined for #{object.class.name}" unless machine
|
236
|
+
|
237
|
+
state = machine.states[state_name]
|
238
|
+
raise ArgumentError, "State '#{state_name}' is not defined in state machine '#{machine_name}'" unless state
|
239
|
+
|
240
|
+
state.matches?(object.send(machine_name))
|
241
|
+
end
|
201
242
|
end
|
202
243
|
end
|
data/lib/state_machines/event.rb
CHANGED
@@ -104,7 +104,7 @@ module StateMachines
|
|
104
104
|
|
105
105
|
# Only a certain subset of explicit options are allowed for transition
|
106
106
|
# requirements
|
107
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - %i[from to on except_from except_to except_on if unless]).empty?
|
107
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state) if (options.keys - %i[from to on except_from except_to except_on if unless if_state unless_state if_all_states unless_all_states if_any_state unless_any_state]).empty?
|
108
108
|
|
109
109
|
branches << branch = Branch.new(options.merge(on: name))
|
110
110
|
@known_states |= branch.known_states
|