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.
- checksums.yaml +7 -0
- data/state_machines-0.201.0/LICENSE.txt +23 -0
- data/state_machines-0.201.0/README.md +1024 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
- data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
- data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
- data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
- data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
- data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
- data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
- data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
- data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
- data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
- data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
- data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
- data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
- data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
- data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
- data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
- data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
- data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
- data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
- data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
- data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
- data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
- data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
- data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
- data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
- data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
- data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
- data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
- data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
- data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
- data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
- data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
- data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
- data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
- data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
- data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
- data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
- data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
- data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
- data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
- data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
- data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
- data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines.rb +6 -0
- data/ultra-smart-sys.gemspec +12 -0
- metadata +96 -0
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+

|
|
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
|