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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc7dae3a88c68f1327da47debac2546fde7c4459346dbfefaabbc4bed5dc5554
4
- data.tar.gz: 2fbfc7157580635bd546b9096138677d44b58a92578d6341667a1722c91ccb66
3
+ metadata.gz: 247ae1ee6fa7beb6ac29a68dfd400ea41a3923b973d842a5adb2ef78960b3266
4
+ data.tar.gz: 7240a29d3d87740ade0d0197e9c4c0192176023f23e453d1ba659e8be905afef
5
5
  SHA512:
6
- metadata.gz: 4b5ba6ecdeb4dd612c6865657e0a0b590d3fa67fed522359c7b388ab88401283d953d857fc73fab2fbeb97388c5bb71b5f4e125d70366a03547aea91b991ab35
7
- data.tar.gz: 07c96c5556ae74d933de3cf1b132cfda9144404361cca87c24c14694e2158e5f0437d65a4ab08b857dca8ad0c453d7fdc674abeb770c528d7ec45e4b060c8ddd
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
- case [query[:guard], if_condition, unless_condition]
186
- in [false, _, _]
187
- true
188
- in [_, nil, nil]
189
- true
190
- in [_, if_conds, nil] if if_conds
191
- Array(if_conds).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
192
- in [_, nil, unless_conds] if unless_conds
193
- Array(unless_conds).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
194
- in [_, if_conds, unless_conds] if if_conds || unless_conds
195
- Array(if_conds).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) } &&
196
- Array(unless_conds).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
197
- else
198
- true
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StateMachines
4
- VERSION = '0.40.0'
4
+ VERSION = '0.50.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.40.0
4
+ version: 0.50.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih