finite_machine 0.13.0 → 0.14.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 +4 -4
- data/CHANGELOG.md +42 -23
- data/LICENSE.txt +1 -1
- data/README.md +207 -145
- data/lib/finite_machine/catchable.rb +20 -12
- data/lib/finite_machine/choice_merger.rb +2 -2
- data/lib/finite_machine/definition.rb +54 -17
- data/lib/finite_machine/dsl.rb +35 -5
- data/lib/finite_machine/env.rb +1 -1
- data/lib/finite_machine/events_map.rb +5 -6
- data/lib/finite_machine/hook_event.rb +1 -1
- data/lib/finite_machine/hooks.rb +10 -5
- data/lib/finite_machine/message_queue.rb +72 -26
- data/lib/finite_machine/observer.rb +38 -26
- data/lib/finite_machine/safety.rb +6 -6
- data/lib/finite_machine/state_machine.rb +19 -16
- data/lib/finite_machine/state_parser.rb +8 -8
- data/lib/finite_machine/subscribers.rb +1 -1
- data/lib/finite_machine/threadable.rb +1 -1
- data/lib/finite_machine/transition.rb +6 -5
- data/lib/finite_machine/transition_builder.rb +4 -4
- data/lib/finite_machine/transition_event.rb +1 -1
- data/lib/finite_machine/version.rb +1 -1
- data/lib/finite_machine.rb +6 -6
- metadata +18 -13
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
<div align="center">
|
2
|
-
<a href="
|
2
|
+
<a href="https://piotrmurach.github.io/finite_machine/"><img width="236" src="https://github.com/piotrmurach/finite_machine/raw/master/assets/finite_machine_logo.png" alt="finite machine logo" /></a>
|
3
3
|
</div>
|
4
4
|
|
5
5
|
# FiniteMachine
|
6
6
|
|
7
7
|
[][gem]
|
8
|
-
[][gh_actions_ci]
|
9
9
|
[][appveyor]
|
10
10
|
[][codeclimate]
|
11
11
|
[][coverage]
|
@@ -13,7 +13,7 @@
|
|
13
13
|
[][gitter]
|
14
14
|
|
15
15
|
[gem]: http://badge.fury.io/rb/finite_machine
|
16
|
-
[
|
16
|
+
[gh_actions_ci]: https://github.com/piotrmurach/finite_machine/actions?query=workflow%3ACI
|
17
17
|
[appveyor]: https://ci.appveyor.com/project/piotrmurach/finite-machine
|
18
18
|
[codeclimate]: https://codeclimate.com/github/piotrmurach/finite_machine
|
19
19
|
[coverage]: https://coveralls.io/github/piotrmurach/finite_machine?branch=master
|
@@ -38,7 +38,7 @@
|
|
38
38
|
|
39
39
|
Add this line to your application's Gemfile:
|
40
40
|
|
41
|
-
gem
|
41
|
+
gem "finite_machine"
|
42
42
|
|
43
43
|
Then execute:
|
44
44
|
|
@@ -119,34 +119,35 @@ fm = FiniteMachine.new do
|
|
119
119
|
event :stop, :green => :red
|
120
120
|
|
121
121
|
on_before(:ready) { |event| ... }
|
122
|
-
|
123
|
-
|
122
|
+
on_exit(:yellow) { |event| ... }
|
123
|
+
on_enter(:green) { |event| ... }
|
124
|
+
on_after(:stop) { |event| ... }
|
124
125
|
end
|
125
126
|
```
|
126
127
|
|
127
|
-
|
128
|
+
By calling the `new` method on **FiniteMachine**, you gain access to a powerful DSL for expressing transitions and registering callbacks.
|
128
129
|
|
129
|
-
Having declared the states and transitions
|
130
|
+
Having declared the states and transitions, you can check current state:
|
130
131
|
|
131
132
|
```ruby
|
132
133
|
fm.current # => :red
|
133
134
|
````
|
134
135
|
|
135
|
-
And trigger transitions using the `trigger`:
|
136
|
+
And then trigger transitions using the `trigger`:
|
136
137
|
|
137
138
|
```ruby
|
138
139
|
fm.trigger(:ready)
|
139
140
|
```
|
140
141
|
|
141
|
-
|
142
|
+
Or you can use direct method calls:
|
142
143
|
|
143
|
-
|
144
|
-
|
145
|
-
|
144
|
+
```ruby
|
145
|
+
fm.ready
|
146
|
+
```
|
146
147
|
|
147
|
-
|
148
|
+
Read [States and Transitions](#3-states-and-transitions) and [Callbacks](#4-callbacks) sections for more details.
|
148
149
|
|
149
|
-
Alternatively, you can construct the state machine like a regular object
|
150
|
+
Alternatively, you can construct the state machine like a regular object using the same DSL methods. Similar machine could be reimplemented as follows:
|
150
151
|
|
151
152
|
```ruby
|
152
153
|
fm = FiniteMachine.new(initial: :red)
|
@@ -154,8 +155,9 @@ fm.event(:ready, :red => :yellow)
|
|
154
155
|
fm.event(:go, :yellow => :green)
|
155
156
|
fm.event(:stop, :green => :red)
|
156
157
|
fm.on_before(:ready) { |event| ... }
|
157
|
-
fm.
|
158
|
-
fm.
|
158
|
+
fm.on_exit(:yellow) { |event| ... }
|
159
|
+
fm.on_enter(:green) { |event| ... }
|
160
|
+
fm.on_after(:stop) { |event| ... }
|
159
161
|
```
|
160
162
|
|
161
163
|
## 2. API
|
@@ -178,19 +180,19 @@ end
|
|
178
180
|
Alternatively, you can skip block definition and instead call DSL methods directly on the state machine instance:
|
179
181
|
|
180
182
|
```ruby
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
183
|
+
fm = FiniteMachine.new
|
184
|
+
fm.initial(:green)
|
185
|
+
fm.event(:slow, :green => :yellow)
|
186
|
+
fm.event(:stop, :yellow => :red)
|
187
|
+
fm.event(:ready,:red => :yellow)
|
188
|
+
fm.event(:go, :yellow => :green)
|
187
189
|
```
|
188
190
|
|
189
191
|
As a guiding rule, any method exposed via DSL is available as a regular method call on the state machine instance.
|
190
192
|
|
191
193
|
### 2.2 define
|
192
194
|
|
193
|
-
To create a reusable definition for a state machine use `define` method. By calling `define` you're creating an anonymous class that can act as a factory for state machines. For example, below we create a
|
195
|
+
To create a reusable definition for a state machine use `define` method. By calling `define` you're creating an anonymous class that can act as a factory for state machines. For example, below we create a `TrafficLights` class that contains our state machine definition:
|
194
196
|
|
195
197
|
```ruby
|
196
198
|
TrafficLights = FiniteMachine.define do
|
@@ -203,7 +205,7 @@ TrafficLights = FiniteMachine.define do
|
|
203
205
|
end
|
204
206
|
```
|
205
207
|
|
206
|
-
Then
|
208
|
+
Then we can create however many instance of above class:
|
207
209
|
|
208
210
|
```ruby
|
209
211
|
lights_fm_a = TrafficLights.new
|
@@ -217,7 +219,7 @@ lights_fm_a.current # => :green
|
|
217
219
|
lights_fm_b.current # => :green
|
218
220
|
```
|
219
221
|
|
220
|
-
|
222
|
+
We can then trigger event for one instance and not the other:
|
221
223
|
|
222
224
|
```ruby
|
223
225
|
lights_fm_a.slow
|
@@ -267,7 +269,7 @@ fm.current # => :green
|
|
267
269
|
Or by passing named argument `:initial` like so:
|
268
270
|
|
269
271
|
```ruby
|
270
|
-
fm = FiniteMachine.new
|
272
|
+
fm = FiniteMachine.new(initial: :green) do
|
271
273
|
...
|
272
274
|
end
|
273
275
|
```
|
@@ -286,7 +288,7 @@ fm.init # execute initial transition
|
|
286
288
|
fm.current # => :green
|
287
289
|
```
|
288
290
|
|
289
|
-
If your target object already has `init` method or one of the events names
|
291
|
+
If your target object already has `init` method or one of the events names redefines `init`, you can use different name by passing `:event` option to `initial` helper.
|
290
292
|
|
291
293
|
```ruby
|
292
294
|
fm = FiniteMachine.new do
|
@@ -301,7 +303,7 @@ fm.start # => call the renamed event
|
|
301
303
|
fm.current # => :green
|
302
304
|
```
|
303
305
|
|
304
|
-
By default the `initial` does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the `silent` option set to `false` like so
|
306
|
+
By default the `initial` does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the `silent` option set to `false` like so:
|
305
307
|
|
306
308
|
```ruby
|
307
309
|
fm = FiniteMachine.new do
|
@@ -355,7 +357,7 @@ And the terminal state can be checked using `terminated?`:
|
|
355
357
|
|
356
358
|
```ruby
|
357
359
|
fm.decline
|
358
|
-
fm.terminated?
|
360
|
+
fm.terminated? # => true
|
359
361
|
```
|
360
362
|
|
361
363
|
### 2.6 is?
|
@@ -376,18 +378,18 @@ fm.yellow? # => false
|
|
376
378
|
|
377
379
|
### 2.7 trigger
|
378
380
|
|
379
|
-
|
381
|
+
Transition events can be fired by calling the `trigger` method with the event name and remaining arguments as data. The return value is either `true` or `false` depending whether the transition succeeded or not:
|
380
382
|
|
381
383
|
```ruby
|
382
384
|
fm.trigger(:ready) # => true
|
383
|
-
fm.trigger(:ready,
|
385
|
+
fm.trigger(:ready, "one", "two", "three") # => true
|
384
386
|
```
|
385
387
|
|
386
|
-
By default the **FiniteMachine** automatically converts all the transition event names into methods:
|
388
|
+
By default, the **FiniteMachine** automatically converts all the transition event names into methods:
|
387
389
|
|
388
390
|
```ruby
|
389
391
|
fm.ready # => true
|
390
|
-
fm.ready(
|
392
|
+
fm.ready("one", "two", "three") # => true
|
391
393
|
```
|
392
394
|
|
393
395
|
Please see [States and Transitions](#3-states-and-transitions) for in-depth treatment of firing transitions.
|
@@ -395,8 +397,7 @@ Please see [States and Transitions](#3-states-and-transitions) for in-depth trea
|
|
395
397
|
|
396
398
|
#### 2.7.1 `:auto_methods`
|
397
399
|
|
398
|
-
By default all event names will be converted by **FiniteMachine** into method names. This also means that you won't be able to use event names such as `:fail` or `:trigger` as these are already defined on the machine instance. In situations when you wish to use any event name for your event names use `:auto_methods` keyword to disable automatic methods generation. For example, to define `:fail` event:
|
399
|
-
|
400
|
+
By default, all event names will be converted by **FiniteMachine** into method names. This also means that you won't be able to use event names such as `:fail` or `:trigger` as these are already defined on the machine instance. In situations when you wish to use any event name for your event names use `:auto_methods` keyword to disable automatic methods generation. For example, to define `:fail` event:
|
400
401
|
|
401
402
|
```ruby
|
402
403
|
fm = FiniteMachine.new(auto_methods: false) do
|
@@ -431,7 +432,7 @@ fm = FiniteMachine.new do
|
|
431
432
|
initial :green
|
432
433
|
|
433
434
|
event :slow, :green => :yellow
|
434
|
-
event :stop, :yellow => :red, if:
|
435
|
+
event :stop, :yellow => :red, if: ->(_, param) { :breaks == param }
|
435
436
|
end
|
436
437
|
|
437
438
|
fm.can?(:slow) # => true
|
@@ -446,45 +447,45 @@ fm.can?(:stop, :no_breaks) # => false
|
|
446
447
|
|
447
448
|
If you need to execute some external code in the context of the current state machine, pass that object as a first argument to `new` method.
|
448
449
|
|
449
|
-
Assuming we have a simple `
|
450
|
+
Assuming we have a simple `Engine` class that holds an internal state whether the car's engine is on or off:
|
450
451
|
|
451
452
|
```ruby
|
452
|
-
class
|
453
|
+
class Engine
|
453
454
|
def initialize
|
454
|
-
@
|
455
|
+
@engine = false
|
455
456
|
end
|
456
457
|
|
457
|
-
def
|
458
|
-
@
|
458
|
+
def turn_on
|
459
|
+
@engine = true
|
459
460
|
end
|
460
461
|
|
461
|
-
def
|
462
|
-
@
|
462
|
+
def turn_off
|
463
|
+
@engine = false
|
463
464
|
end
|
464
465
|
|
465
466
|
def engine_on?
|
466
|
-
@
|
467
|
+
@engine
|
467
468
|
end
|
468
469
|
end
|
469
470
|
```
|
470
471
|
|
471
|
-
And given an instance of `
|
472
|
+
And given an instance of `Engine` class:
|
472
473
|
|
473
474
|
```ruby
|
474
|
-
|
475
|
+
engine = Engine.new
|
475
476
|
```
|
476
477
|
|
477
478
|
You can provide a context to a state machine by passing it as a first argument to a `new` call. You can then reference this context inside the callbacks by calling the `target` helper:
|
478
479
|
|
479
480
|
```ruby
|
480
|
-
fm = FiniteMachine.new(
|
481
|
+
fm = FiniteMachine.new(engine) do
|
481
482
|
initial :neutral
|
482
483
|
|
483
|
-
event :start, :neutral => :one,
|
484
|
+
event :start, :neutral => :one, unless: "engine_on?"
|
484
485
|
event :stop, :one => :neutral
|
485
486
|
|
486
|
-
|
487
|
-
|
487
|
+
on_before_start { |event| target.turn_on }
|
488
|
+
on_after_stop { |event| target.turn_off }
|
488
489
|
end
|
489
490
|
```
|
490
491
|
|
@@ -495,18 +496,52 @@ For more complex example see [Integration](#7-integration) section.
|
|
495
496
|
If you wish to better express the intention behind the context object, in particular when calling actions in callbacks, you can use the `:alias_target` option:
|
496
497
|
|
497
498
|
```ruby
|
498
|
-
|
499
|
+
engine = Engine.new
|
500
|
+
|
501
|
+
fm = FiniteMachine.new(engine, alias_target: :engine) do
|
502
|
+
initial :neutral
|
503
|
+
|
504
|
+
event :start, :neutral => :one, unless: "engine_on?"
|
505
|
+
event :stop, :none => :neutral, if: "engine_on?"
|
506
|
+
|
507
|
+
on_before_start { |event| engine.turn_on }
|
508
|
+
on_after_stop { |event| engine.turn_off }
|
509
|
+
end
|
510
|
+
```
|
511
|
+
|
512
|
+
Alternatively, you can use the `alias_target` helper method:
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
engine = Engine.new
|
516
|
+
|
517
|
+
Car = FiniteMachine.define do
|
518
|
+
alias_target :engine
|
499
519
|
|
500
|
-
fm = FiniteMachine.new(car, alias_target: :car) do
|
501
520
|
initial :neutral
|
502
521
|
|
503
522
|
event :start, :neutral => :one, if: "engine_on?"
|
523
|
+
event :stop, :none => :neutral, if: "engine_on?"
|
504
524
|
|
505
|
-
|
506
|
-
|
525
|
+
on_before_start { |event| engine.turn_on }
|
526
|
+
on_after_stop { |event| engine.turn_off }
|
507
527
|
end
|
508
528
|
```
|
509
529
|
|
530
|
+
Then to link `Car` definition with `Engine` instance, pass the `Engine` instance as a first argument:
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
car = Car.new(engine)
|
534
|
+
```
|
535
|
+
|
536
|
+
Triggering `start` event will change `Engine` instance state from `false` to `true`:
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
engine.engine_on? # => false
|
540
|
+
car.start
|
541
|
+
car.current # => :one
|
542
|
+
engine.engine_on? # => true
|
543
|
+
```
|
544
|
+
|
510
545
|
### 2.10 restore!
|
511
546
|
|
512
547
|
In order to set the machine to a given state and thus skip triggering callbacks use the `restore!` method:
|
@@ -543,7 +578,7 @@ in the form of `:from` and `:to` hash keys or by using the state names themselve
|
|
543
578
|
|
544
579
|
```ruby
|
545
580
|
event :start, from: :neutral, to: :first
|
546
|
-
or
|
581
|
+
# or
|
547
582
|
event :start, :neutral => :first
|
548
583
|
```
|
549
584
|
|
@@ -574,7 +609,7 @@ fm.trigger(:ready) # => true
|
|
574
609
|
Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback as well as used by any present guarding conditions.
|
575
610
|
|
576
611
|
```ruby
|
577
|
-
fm.go(
|
612
|
+
fm.go("Piotr!") # => true
|
578
613
|
fm.current # => :green
|
579
614
|
```
|
580
615
|
|
@@ -619,9 +654,7 @@ You can use `any_state` as the name for a given state, for instance:
|
|
619
654
|
|
620
655
|
```ruby
|
621
656
|
event :run, from: any_state, to: :green
|
622
|
-
|
623
|
-
or
|
624
|
-
|
657
|
+
# or
|
625
658
|
event :run, any_state => :green
|
626
659
|
```
|
627
660
|
|
@@ -638,12 +671,12 @@ All the above `run` event definitions will always transition the state machine i
|
|
638
671
|
Another way to specify state transitions under single event name is to group all your state transitions into a single hash like so:
|
639
672
|
|
640
673
|
```ruby
|
641
|
-
fm = FiniteMachine.
|
674
|
+
fm = FiniteMachine.new do
|
642
675
|
initial :initial
|
643
676
|
|
644
677
|
event :bump, :initial => :low,
|
645
|
-
|
646
|
-
|
678
|
+
:low => :medium,
|
679
|
+
:medium => :high
|
647
680
|
end
|
648
681
|
```
|
649
682
|
|
@@ -671,8 +704,8 @@ fm = FiniteMachine.new do
|
|
671
704
|
event :stop, :green => :red
|
672
705
|
end
|
673
706
|
|
674
|
-
|
675
|
-
|
707
|
+
fm.go # no callbacks
|
708
|
+
fm.stop # callbacks are fired
|
676
709
|
```
|
677
710
|
|
678
711
|
### 3.7 Logging transitions
|
@@ -680,7 +713,7 @@ fms.stop # callbacks are fired
|
|
680
713
|
To help debug your state machine, **FiniteMachine** provides `:log_transitions` option.
|
681
714
|
|
682
715
|
```ruby
|
683
|
-
FiniteMachine.new
|
716
|
+
FiniteMachine.new(log_transitions: true) do
|
684
717
|
...
|
685
718
|
end
|
686
719
|
```
|
@@ -707,11 +740,11 @@ fm.current # => :green
|
|
707
740
|
Condition by default receives the current context, which is the current state machine instance, followed by extra arguments.
|
708
741
|
|
709
742
|
```ruby
|
710
|
-
|
743
|
+
fm = FiniteMachine.new do
|
711
744
|
initial :red
|
712
745
|
|
713
746
|
event :go, :red => :green,
|
714
|
-
if: ->
|
747
|
+
if: ->(context, a) { context.current == a }
|
715
748
|
end
|
716
749
|
|
717
750
|
fm.go(:yellow) # doesn't transition
|
@@ -721,46 +754,48 @@ fm.go # raises ArgumentError
|
|
721
754
|
**Note** If you specify condition with a given number of arguments then you need to call an event with the exact number of arguments, otherwise you will get `ArgumentError`. Thus in above scenario to prevent errors specify condition like so:
|
722
755
|
|
723
756
|
```ruby
|
724
|
-
if: ->
|
757
|
+
if: ->(context, *args) { ... }
|
725
758
|
```
|
726
759
|
|
727
760
|
Provided your **FiniteMachine** is associated with another object through `target` helper. Then the target object together with event arguments will be passed to the `:if` or `:unless` condition scope.
|
728
761
|
|
729
762
|
```ruby
|
730
|
-
class
|
731
|
-
|
763
|
+
class Engine
|
764
|
+
def initialize
|
765
|
+
@engine = false
|
766
|
+
end
|
732
767
|
|
733
|
-
def
|
734
|
-
@
|
768
|
+
def turn_on
|
769
|
+
@engine = true
|
735
770
|
end
|
736
771
|
|
737
|
-
def
|
738
|
-
@
|
772
|
+
def turn_off
|
773
|
+
@engine = false
|
739
774
|
end
|
740
775
|
|
741
776
|
def engine_on?
|
742
|
-
@
|
777
|
+
@engine
|
743
778
|
end
|
744
779
|
end
|
745
780
|
|
746
|
-
|
747
|
-
|
781
|
+
engine = Engine.new
|
782
|
+
engine.turn_on
|
748
783
|
|
749
|
-
|
784
|
+
car = FiniteMachine.new(engine) do
|
750
785
|
initial :neutral
|
751
786
|
|
752
|
-
target
|
753
|
-
|
754
|
-
|
755
|
-
target.engine_on = state
|
756
|
-
target.engine_on?
|
757
|
-
}
|
787
|
+
event :start, :neutral => :one, if: ->(target, state) do
|
788
|
+
state ? target.engine_on : target.engine_off
|
789
|
+
end
|
758
790
|
end
|
759
791
|
|
760
792
|
fm.start(false)
|
761
|
-
fm.current
|
793
|
+
fm.current # => :neutral
|
794
|
+
engine.engine_on? # => false
|
795
|
+
|
762
796
|
fm.start(true)
|
763
|
-
fm.current
|
797
|
+
fm.current # => :one
|
798
|
+
engine.engine_on? # => true
|
764
799
|
```
|
765
800
|
|
766
801
|
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [4.9 Cancelling callbacks](#49-cancelling-inside-callbacks)
|
@@ -770,11 +805,9 @@ When the one-liner conditions are not enough for your needs, you can perform con
|
|
770
805
|
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
|
771
806
|
|
772
807
|
```ruby
|
773
|
-
|
808
|
+
fm = FiniteMachine.new(engine) do
|
774
809
|
initial :neutral
|
775
810
|
|
776
|
-
target car
|
777
|
-
|
778
811
|
event :start, :neutral => :one, if: :engine_on?
|
779
812
|
end
|
780
813
|
```
|
@@ -784,11 +817,9 @@ end
|
|
784
817
|
Finally, it's possible to use string that will be evaluated using `eval` and needs to contain valid Ruby code. It should only be used when the string represents a short condition.
|
785
818
|
|
786
819
|
```ruby
|
787
|
-
|
820
|
+
fm = FiniteMachine.new(engine) do
|
788
821
|
initial :neutral
|
789
822
|
|
790
|
-
target car
|
791
|
-
|
792
823
|
event :start, :neutral => :one, if: "engine_on?"
|
793
824
|
end
|
794
825
|
```
|
@@ -798,7 +829,7 @@ end
|
|
798
829
|
When multiple conditions define whether or not a transition should happen, an Array can be used. Furthermore, you can apply both `:if` and `:unless` to the same transition.
|
799
830
|
|
800
831
|
```ruby
|
801
|
-
|
832
|
+
fm = FiniteMachine.new do
|
802
833
|
initial :green
|
803
834
|
|
804
835
|
event :slow, :green => :yellow,
|
@@ -812,27 +843,27 @@ The transition only runs when all the `:if` conditions and none of the `unless`
|
|
812
843
|
|
813
844
|
### 3.9 Choice pseudostates
|
814
845
|
|
815
|
-
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to
|
846
|
+
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to select only one outgoing transition.
|
816
847
|
|
817
848
|
You can implement the conditional branch as ordinary events grouped under the same name and use familiar `:if/:unless` conditions:
|
818
849
|
|
819
850
|
```ruby
|
820
|
-
|
851
|
+
fm = FiniteMachine.define do
|
821
852
|
initial :green
|
822
853
|
|
823
854
|
event :next, :green => :yellow, if: -> { false }
|
824
855
|
event :next, :green => :red, if: -> { true }
|
825
856
|
end
|
826
857
|
|
827
|
-
|
828
|
-
|
829
|
-
|
858
|
+
fm.current # => :green
|
859
|
+
fm.next
|
860
|
+
fm.current # => :red
|
830
861
|
```
|
831
862
|
|
832
863
|
The same conditional logic can be implemented using much shorter and more descriptive style using `choice` method:
|
833
864
|
|
834
865
|
```ruby
|
835
|
-
|
866
|
+
fm = FiniteMachine.new do
|
836
867
|
initial :green
|
837
868
|
|
838
869
|
event :next, from: :green do
|
@@ -841,9 +872,9 @@ fsm = FiniteMachine.new do
|
|
841
872
|
end
|
842
873
|
end
|
843
874
|
|
844
|
-
|
845
|
-
|
846
|
-
|
875
|
+
fm.current # => :green
|
876
|
+
fm.next
|
877
|
+
fm.current # => :red
|
847
878
|
```
|
848
879
|
|
849
880
|
#### 3.9.1 Dynamic choice conditions
|
@@ -851,19 +882,19 @@ fsm.current # => :red
|
|
851
882
|
Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
|
852
883
|
|
853
884
|
```ruby
|
854
|
-
|
885
|
+
fm = FiniteMachine.new do
|
855
886
|
initial :green
|
856
887
|
|
857
888
|
event :next, from: :green do
|
858
|
-
choice :yellow, if: ->
|
859
|
-
choice :red, if: ->
|
889
|
+
choice :yellow, if: ->(context, a) { a < 1 }
|
890
|
+
choice :red, if: ->(context, a) { a > 1 }
|
860
891
|
default :red
|
861
892
|
end
|
862
893
|
end
|
863
894
|
|
864
|
-
|
865
|
-
|
866
|
-
|
895
|
+
fm.current # => :green
|
896
|
+
fm.next(0)
|
897
|
+
fm.current # => :yellow
|
867
898
|
```
|
868
899
|
|
869
900
|
If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the `default` state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify `default` choice.
|
@@ -883,7 +914,7 @@ FiniteMachine.new do
|
|
883
914
|
end
|
884
915
|
```
|
885
916
|
|
886
|
-
|
917
|
+
Or from any state using the `:any` state name like so:
|
887
918
|
|
888
919
|
```ruby
|
889
920
|
FiniteMachine.new do
|
@@ -909,7 +940,7 @@ You can register a callback to listen for state transitions and events triggered
|
|
909
940
|
Use the state or event name as a first parameter to the callback helper followed by block with event argument and a list arguments that you expect to receive like so:
|
910
941
|
|
911
942
|
```ruby
|
912
|
-
on_enter
|
943
|
+
on_enter(:green) { |event, a, b, c| ... }
|
913
944
|
```
|
914
945
|
|
915
946
|
When you subscribe to the `:green` state change, the callback will be called whenever someone triggers event that transitions in or out of that state. The same will happen on subscription to event `ready`, namely, the callback will be called each time the state transition method is triggered regardless of the states it transitions from or to.
|
@@ -922,13 +953,17 @@ fm = FiniteMachine.new do
|
|
922
953
|
event :go, :yellow => :green
|
923
954
|
event :stop, :green => :red
|
924
955
|
|
925
|
-
on_before :ready
|
926
|
-
|
927
|
-
|
956
|
+
on_before :ready do |event, time1, time2, time3|
|
957
|
+
puts "#{time1} #{time2} #{time3} Go!" }
|
958
|
+
end
|
959
|
+
on_before :go do |event, name|
|
960
|
+
puts "Going fast #{name}"
|
961
|
+
end
|
962
|
+
on_before(:stop) { |event| ... }
|
928
963
|
end
|
929
964
|
|
930
965
|
fm.ready(1, 2, 3)
|
931
|
-
fm.go(
|
966
|
+
fm.go("Piotr!")
|
932
967
|
```
|
933
968
|
|
934
969
|
**Note** Regardless of how the state is entered or exited, all the associated callbacks will be executed. This provides means for guaranteed initialization and cleanup.
|
@@ -982,9 +1017,9 @@ Then by calling `go` event the following callbacks sequence will be executed:
|
|
982
1017
|
|
983
1018
|
All callbacks as a first argument yielded to a block receive the `TransitionEvent` object with the following attributes:
|
984
1019
|
|
985
|
-
* `name
|
986
|
-
* `from
|
987
|
-
* `to
|
1020
|
+
* `name` - the event name`
|
1021
|
+
* `from` - the state transitioning from`
|
1022
|
+
* `to` - the state transitioning to`
|
988
1023
|
|
989
1024
|
followed by the rest of arguments that were passed to the event method.
|
990
1025
|
|
@@ -994,33 +1029,43 @@ fm = FiniteMachine.new do
|
|
994
1029
|
|
995
1030
|
event :ready, :red => :yellow
|
996
1031
|
|
997
|
-
|
1032
|
+
on_before_ready do |event, time|
|
998
1033
|
puts "lights switching from #{event.from} to #{event.to} in #{time} seconds"
|
999
|
-
|
1034
|
+
end
|
1000
1035
|
end
|
1001
1036
|
|
1002
|
-
fm.ready(3)
|
1037
|
+
fm.ready(3)
|
1038
|
+
# => "lights switching from red to yellow in 3 seconds"
|
1003
1039
|
```
|
1004
1040
|
|
1005
1041
|
### 4.6 Duplicate callbacks
|
1006
1042
|
|
1007
1043
|
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
1008
1044
|
|
1045
|
+
Given the following state machine instance:
|
1046
|
+
|
1009
1047
|
```ruby
|
1010
1048
|
fm = FiniteMachine.new do
|
1011
1049
|
initial :green
|
1012
1050
|
|
1013
1051
|
event :slow, :green => :yellow
|
1014
1052
|
|
1015
|
-
on_enter(:yellow) {
|
1016
|
-
on_enter(:yellow) {
|
1053
|
+
on_enter(:yellow) { puts "this is run first" }
|
1054
|
+
on_enter(:yellow) { puts "then this is run" }
|
1017
1055
|
end
|
1018
|
-
|
1056
|
+
```
|
1057
|
+
|
1058
|
+
Triggerring the `:slow` event results in:
|
1059
|
+
|
1060
|
+
```ruby
|
1061
|
+
fm.slow
|
1062
|
+
# => "this is run first"
|
1063
|
+
# => "then this is run"
|
1019
1064
|
```
|
1020
1065
|
|
1021
1066
|
### 4.7 Fluid callbacks
|
1022
1067
|
|
1023
|
-
Callbacks can also be specified as full method calls
|
1068
|
+
Callbacks can also be specified as full method calls separated with underscores:
|
1024
1069
|
|
1025
1070
|
```ruby
|
1026
1071
|
fm = FiniteMachine.define do
|
@@ -1083,10 +1128,15 @@ fm = FiniteMachine.new do
|
|
1083
1128
|
event :forward, [:reverse, :neutral] => :one
|
1084
1129
|
event :back, [:neutral, :one] => :reverse
|
1085
1130
|
|
1086
|
-
on_enter_reverse { |event| forward(
|
1131
|
+
on_enter_reverse { |event| forward("Piotr!") }
|
1087
1132
|
on_exit_reverse { |event, name| puts "Go #{name}" }
|
1088
1133
|
end
|
1089
|
-
|
1134
|
+
```
|
1135
|
+
|
1136
|
+
Then triggerring `:back` event gives:
|
1137
|
+
|
1138
|
+
```ruby
|
1139
|
+
fm.back # => Go Piotr!
|
1090
1140
|
```
|
1091
1141
|
|
1092
1142
|
For more complex example see [Integration](#7-integration) section.
|
@@ -1097,7 +1147,7 @@ A simple way to prevent transitions is to use [3 Conditional transitions](#3-con
|
|
1097
1147
|
|
1098
1148
|
There are times when you want to cancel transition in a callback. For example, you have logic which allows transition to happen only under certain complex conditions. Using `cancel_event` inside the `on_(enter|transition|exit)` or `on_(before|after)` callbacks will stop all the callbacks from firing and prevent current transition from happening.
|
1099
1149
|
|
1100
|
-
For example,
|
1150
|
+
For example, the following state machine cancels any event leaving `:red` state:
|
1101
1151
|
|
1102
1152
|
```ruby
|
1103
1153
|
fm = FiniteMachine.new do
|
@@ -1112,7 +1162,12 @@ fm = FiniteMachine.new do
|
|
1112
1162
|
cancel_event
|
1113
1163
|
end
|
1114
1164
|
end
|
1165
|
+
```
|
1166
|
+
|
1167
|
+
Then firing `:ready` event will not transition out of the current `:red` state:
|
1115
1168
|
|
1169
|
+
```ruby
|
1170
|
+
fm.current # => :red
|
1116
1171
|
fm.ready
|
1117
1172
|
fm.current # => :red
|
1118
1173
|
```
|
@@ -1122,21 +1177,18 @@ fm.current # => :red
|
|
1122
1177
|
By default all callbacks are run synchronously. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
|
1123
1178
|
|
1124
1179
|
```ruby
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
Or
|
1129
|
-
|
1130
|
-
```ruby
|
1131
|
-
on_enter_green(:async) { |event| }
|
1180
|
+
on_enter(:green, :async) do |event| ... end
|
1181
|
+
# or
|
1182
|
+
on_enter_green(:async) { |event| }
|
1132
1183
|
```
|
1133
1184
|
|
1134
1185
|
This will ensure that when the callback is fired it will run in separate thread outside of the main execution thread.
|
1135
1186
|
|
1136
|
-
|
1137
1187
|
### 4.11 Instance callbacks
|
1138
1188
|
|
1139
|
-
When defining callbacks you are not limited to the
|
1189
|
+
When defining callbacks you are not limited to the **FiniteMachine** block definition. After creating an instance, you can register callbacks the same way as before by calling `on` and supplying the type of notification and state/event you are interested in.
|
1190
|
+
|
1191
|
+
For example, given the following state machine:
|
1140
1192
|
|
1141
1193
|
```ruby
|
1142
1194
|
fm = FiniteMachine.new do
|
@@ -1146,10 +1198,14 @@ fm = FiniteMachine.new do
|
|
1146
1198
|
event :go, :yellow => :green
|
1147
1199
|
event :stop, :green => :red
|
1148
1200
|
end
|
1201
|
+
```
|
1149
1202
|
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1203
|
+
We can add callbacks as follows:
|
1204
|
+
|
1205
|
+
```ruby
|
1206
|
+
fm.on_enter(:yellow) { |event| ... }
|
1207
|
+
# or
|
1208
|
+
fm.en_enter_yellow { |event| ... }
|
1153
1209
|
```
|
1154
1210
|
|
1155
1211
|
## 5. Error Handling
|
@@ -1174,7 +1230,7 @@ fm = FiniteMachine.new do
|
|
1174
1230
|
raise exception
|
1175
1231
|
end
|
1176
1232
|
|
1177
|
-
handle FiniteMachine::TransitionError, with:
|
1233
|
+
handle FiniteMachine::TransitionError, with: -> { |exception| ... }
|
1178
1234
|
end
|
1179
1235
|
```
|
1180
1236
|
|
@@ -1195,7 +1251,7 @@ fm = FiniteMachine.new(logger) do
|
|
1195
1251
|
event :slow, :green => :yellow
|
1196
1252
|
event :stop, :yellow => :red
|
1197
1253
|
|
1198
|
-
handle
|
1254
|
+
handle "InvalidStateError", with: :log_error
|
1199
1255
|
end
|
1200
1256
|
```
|
1201
1257
|
|
@@ -1223,7 +1279,9 @@ class Engine < FiniteMachine::Definition
|
|
1223
1279
|
target.turn_reverse_lights_off
|
1224
1280
|
end
|
1225
1281
|
|
1226
|
-
handle FiniteMachine::InvalidStateError do |exception|
|
1282
|
+
handle FiniteMachine::InvalidStateError do |exception|
|
1283
|
+
...
|
1284
|
+
end
|
1227
1285
|
end
|
1228
1286
|
```
|
1229
1287
|
|
@@ -1260,7 +1318,7 @@ engine.back
|
|
1260
1318
|
car.reverse_lights? # => true
|
1261
1319
|
```
|
1262
1320
|
|
1263
|
-
Alternatively, create method inside the `Car` that will do the integration like so
|
1321
|
+
Alternatively, create method inside the `Car` that will do the integration like so:
|
1264
1322
|
|
1265
1323
|
```ruby
|
1266
1324
|
class Car
|
@@ -1394,7 +1452,7 @@ class Account < ActiveRecord::Base
|
|
1394
1452
|
event :authorize, :pending => :access
|
1395
1453
|
|
1396
1454
|
on_enter do |event|
|
1397
|
-
target.state =
|
1455
|
+
target.state = event.to
|
1398
1456
|
end
|
1399
1457
|
end
|
1400
1458
|
end
|
@@ -1436,6 +1494,10 @@ Creating a standalone **FiniteMachine** brings a number of benefits, one of them
|
|
1436
1494
|
4. Push to the branch (`git push origin my-new-feature`)
|
1437
1495
|
5. Create new Pull Request
|
1438
1496
|
|
1497
|
+
## Code of Conduct
|
1498
|
+
|
1499
|
+
Everyone interacting in the FiniteMachine project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/finite_machine/blob/master/CODE_OF_CONDUCT.md).
|
1500
|
+
|
1439
1501
|
## Copyright
|
1440
1502
|
|
1441
1503
|
Copyright (c) 2014 Piotr Murach. See LICENSE for further details.
|