finite_machine 0.13.0 → 0.14.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 Version](https://badge.fury.io/rb/finite_machine.svg)][gem]
|
8
|
-
[![
|
8
|
+
[![Actions CI](https://github.com/piotrmurach/finite_machine/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
|
9
9
|
[![Build status](https://ci.appveyor.com/api/projects/status/8ho4ijacpr7b4f4t?svg=true)][appveyor]
|
10
10
|
[![Code Climate](https://codeclimate.com/github/piotrmurach/finite_machine/badges/gpa.svg)][codeclimate]
|
11
11
|
[![Coverage Status](https://coveralls.io/repos/github/piotrmurach/finite_machine/badge.svg?branch=master)][coverage]
|
@@ -13,7 +13,7 @@
|
|
13
13
|
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)][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.
|