finite_machine 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +154 -46
- data/lib/finite_machine.rb +1 -0
- data/lib/finite_machine/choice_merger.rb +44 -0
- data/lib/finite_machine/dsl.rb +17 -10
- data/lib/finite_machine/event.rb +36 -5
- data/lib/finite_machine/observer.rb +1 -1
- data/lib/finite_machine/state_machine.rb +22 -5
- data/lib/finite_machine/state_parser.rb +3 -1
- data/lib/finite_machine/transition.rb +43 -10
- data/lib/finite_machine/transition_event.rb +2 -2
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/async_events_spec.rb +1 -1
- data/spec/unit/callbacks_spec.rb +29 -8
- data/spec/unit/can_spec.rb +49 -1
- data/spec/unit/choice_spec.rb +137 -0
- data/spec/unit/if_unless_spec.rb +47 -6
- data/spec/unit/initialize_spec.rb +34 -4
- data/spec/unit/states_spec.rb +15 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f31937c5d49cee8c974c90b8339c1e3aa57f4866
|
4
|
+
data.tar.gz: 5386d3a848f6dd01d05d174d165447366c80ed19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8ca32da8076f07a43c53650ebdb99a22ba4e0fb4f721db972caeed7df91fb70feecde074b022e79798b5318c07ab59fddfadc91ddf131d4a4ea4d32b477e946
|
7
|
+
data.tar.gz: 674cc1ae00eb4fafe21adc37721a2bc92f7a482c2755599dbf1c884bb8e1e7f7591bdcaaf09922e54c2204fd3cd64042f43db5b3c9a939c7a2e0c93321ab940a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
0.8.0 (June 22, 2014)
|
2
|
+
|
3
|
+
* Add silent option for state machine events to allow turning on/off
|
4
|
+
selectively callbacks
|
5
|
+
* Ensure that can? & cannot? take into account conditionl logic applied
|
6
|
+
to transitions
|
7
|
+
* Add restore! method to allow to set the state directly without callbacks
|
8
|
+
* Add ability to do dynamic conditional branching using the choice DSL or
|
9
|
+
grouped events with different outgoing transitions [solves #13 and #6 issue]
|
10
|
+
|
1
11
|
0.7.1 (June 8, 2014)
|
2
12
|
|
3
13
|
* Change to relax callback name checks to allow for duplicate state and event names
|
data/README.md
CHANGED
@@ -21,10 +21,10 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
|
|
21
21
|
* observers (pub/sub) for state changes
|
22
22
|
* ability to check reachable states
|
23
23
|
* ability to check for terminal state
|
24
|
-
*
|
24
|
+
* transition guard conditions
|
25
|
+
* dynamic conditional branching
|
25
26
|
* sync and async transitions
|
26
27
|
* sync and async callbacks
|
27
|
-
* nested/composable states (TODO)
|
28
28
|
|
29
29
|
## Installation
|
30
30
|
|
@@ -50,6 +50,7 @@ Or install it yourself as:
|
|
50
50
|
* [1.5 can? and cannot?](#15-can-and-cannot)
|
51
51
|
* [1.6 states](#16-states)
|
52
52
|
* [1.7 target](#17-target)
|
53
|
+
* [1.8 restore!](#18-restore)
|
53
54
|
* [2. Transitions](#2-transitions)
|
54
55
|
* [2.1 Performing transitions](#21-performing-transitions)
|
55
56
|
* [2.2 Forcing transitions](#22-forcing-transitions)
|
@@ -61,26 +62,27 @@ Or install it yourself as:
|
|
61
62
|
* [3.2 Using a Symbol](#32-using-a-symbol)
|
62
63
|
* [3.3 Using a String](#33-using-a-string)
|
63
64
|
* [3.4 Combining transition conditions](#34-combining-transition-conditions)
|
64
|
-
* [4.
|
65
|
-
|
66
|
-
* [
|
67
|
-
* [
|
68
|
-
* [
|
69
|
-
* [4
|
70
|
-
* [
|
71
|
-
* [
|
72
|
-
* [
|
73
|
-
* [
|
74
|
-
* [
|
75
|
-
* [
|
76
|
-
* [
|
77
|
-
* [
|
78
|
-
* [
|
79
|
-
* [5.
|
80
|
-
|
81
|
-
* [6.
|
82
|
-
|
83
|
-
* [7.
|
65
|
+
* [4. Choice pseudostates](#4-choice-pseudostates)
|
66
|
+
* [5. Callbacks](#5-callbacks)
|
67
|
+
* [5.1 on_enter](#51-on_enter)
|
68
|
+
* [5.2 on_transition](#52-on_transition)
|
69
|
+
* [5.3 on_exit](#53-on_exit)
|
70
|
+
* [5.4 on_before](#54-on_before)
|
71
|
+
* [5.5 on_after](#55-on_after)
|
72
|
+
* [5.6 once_on](#56-once_on)
|
73
|
+
* [5.7 Execution sequence](#57-execution-sequence)
|
74
|
+
* [5.8 Parameters](#58-parameters)
|
75
|
+
* [5.9 Same kind of callbacks](#59-same-kind-of-callbacks)
|
76
|
+
* [5.10 Fluid callbacks](#510-fluid-callbacks)
|
77
|
+
* [5.11 Executing methods inside callbacks](#511-executing-methods-inside-callbacks)
|
78
|
+
* [5.12 Defining callbacks](#512-defining-callbacks)
|
79
|
+
* [5.13 Asynchronous callbacks](#513-asynchronous-callbacks)
|
80
|
+
* [5.14 Cancelling inside callbacks](#514-cancelling-inside-callbacks)
|
81
|
+
* [6. Errors](#6-errors)
|
82
|
+
* [6.1 Using target](#61-using-target)
|
83
|
+
* [7. Integration](#7-integration)
|
84
|
+
* [7.1 ActiveRecord](#71-activerecord)
|
85
|
+
* [8. Tips](#7-tips)
|
84
86
|
|
85
87
|
## 1 Usage
|
86
88
|
|
@@ -104,7 +106,7 @@ fm = FiniteMachine.define do
|
|
104
106
|
end
|
105
107
|
```
|
106
108
|
|
107
|
-
As the example demonstrates, by calling the `define` method on **FiniteMachine** you create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#2-transitions) and [Callbacks](#
|
109
|
+
As the example demonstrates, by calling the `define` method on **FiniteMachine** you create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#2-transitions) and [Callbacks](#5-callbacks) sections for more details.
|
108
110
|
|
109
111
|
Alternatively, you can construct the machine like a regular object without using the DSL. The same machine could be reimplemented as follows:
|
110
112
|
|
@@ -128,7 +130,7 @@ The **FiniteMachine** allows you to query the current state by calling the `curr
|
|
128
130
|
|
129
131
|
### 1.2 initial
|
130
132
|
|
131
|
-
There are number of ways to provide the initial state **FiniteMachine** depending on your requirements.
|
133
|
+
There are number of ways to provide the initial state in **FiniteMachine** depending on your requirements.
|
132
134
|
|
133
135
|
By default the **FiniteMachine** will be in the `:none` state and you will need to provide an event to transition out of this state.
|
134
136
|
|
@@ -202,6 +204,19 @@ fm.start
|
|
202
204
|
fm.current # => :green
|
203
205
|
```
|
204
206
|
|
207
|
+
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
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
fm = FiniteMachine.define do
|
211
|
+
initial state: :green, silent: false # => callbacks are triggered
|
212
|
+
|
213
|
+
events {
|
214
|
+
event :slow, :green => :yellow
|
215
|
+
event :stop, :yellow => :red
|
216
|
+
}
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
205
220
|
### 1.3 terminal
|
206
221
|
|
207
222
|
To specify a final state **FiniteMachine** uses the `terminal` method.
|
@@ -255,6 +270,25 @@ fm.cannot?(:ready) # => false
|
|
255
270
|
fm.cannot?(:go) # => true
|
256
271
|
```
|
257
272
|
|
273
|
+
The `can?` and `cannot?` helper methods take into account the `:if` and `:unless` conditions applied to events. The set of values that `:if` or `:unless` condition takes as block parameter can be passed in directly via `can?` and `cannot?` methods' arguments, after the name of the event. For instance,
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
fm = FiniteMachine.define do
|
277
|
+
initial :green
|
278
|
+
|
279
|
+
events {
|
280
|
+
event :slow, :green => :yellow
|
281
|
+
event :stop, :yellow => :red, if: proc { |_, param| :breaks == param }
|
282
|
+
}
|
283
|
+
end
|
284
|
+
fm.can?(:slow) # => true
|
285
|
+
fm.can?(:stop) # => false
|
286
|
+
|
287
|
+
fm.slow
|
288
|
+
fm.can?(:stop, :breaks) # => true
|
289
|
+
fm.can?(:stop, :no_breaks) # => false
|
290
|
+
```
|
291
|
+
|
258
292
|
### 1.6 states
|
259
293
|
|
260
294
|
You can use the `states` method to return an array of all the states for a given state machine.
|
@@ -307,6 +341,7 @@ For more complex example see [Integration](#6-integration) section.
|
|
307
341
|
|
308
342
|
Finally, you can always reference an external context inside the **FiniteMachine** by simply calling `target`, for instance, to reference it inside a callback:
|
309
343
|
|
344
|
+
```ruby
|
310
345
|
car = Car.new
|
311
346
|
|
312
347
|
fm = FiniteMachine.define do
|
@@ -323,6 +358,17 @@ fm = FiniteMachine.define do
|
|
323
358
|
end
|
324
359
|
}
|
325
360
|
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### 1.8 restore!
|
364
|
+
|
365
|
+
In order to set the machine to a given state and thus skip triggering callbacks use the `restore!` method:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
fm.restore!(:neutral)
|
369
|
+
```
|
370
|
+
|
371
|
+
This method may be suitable when used testing your state machine or in restoring the state from datastore.
|
326
372
|
|
327
373
|
## 2 Transitions
|
328
374
|
|
@@ -490,7 +536,7 @@ fm.start(true)
|
|
490
536
|
fm.current # => :one
|
491
537
|
```
|
492
538
|
|
493
|
-
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [
|
539
|
+
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [5.10 Cancelling inside callbacks](#510-cancelling-inside-callbacks)
|
494
540
|
|
495
541
|
### 3.2 Using a Symbol
|
496
542
|
|
@@ -543,7 +589,69 @@ end
|
|
543
589
|
|
544
590
|
The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`.
|
545
591
|
|
546
|
-
## 4
|
592
|
+
## 4 Choice pseudostates
|
593
|
+
|
594
|
+
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to to select only one outgoing transition.
|
595
|
+
|
596
|
+
You can implement the conditional branch as ordinary events grouped under the same name and use familiar `:if/:unless` conditions:
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
fsm = FiniteMachine.define do
|
600
|
+
initial :green
|
601
|
+
|
602
|
+
events {
|
603
|
+
event :next, :green => :yellow, if: -> { false }
|
604
|
+
event :next, :green => :red, if: -> { true }
|
605
|
+
}
|
606
|
+
end
|
607
|
+
|
608
|
+
fsm.current # => :green
|
609
|
+
fsm.next
|
610
|
+
fsm.current # => :red
|
611
|
+
```
|
612
|
+
|
613
|
+
The same conditional logic can be implemented using much shorter and more descriptive style using `choice` method:
|
614
|
+
|
615
|
+
```ruby
|
616
|
+
fsm = FiniteMachine.define do
|
617
|
+
initial :green
|
618
|
+
|
619
|
+
events {
|
620
|
+
event :next, from: :green do
|
621
|
+
choice :yellow, if: -> { false }
|
622
|
+
choice :red, if: -> { true }
|
623
|
+
end
|
624
|
+
}
|
625
|
+
end
|
626
|
+
|
627
|
+
fsm.current # => :green
|
628
|
+
fsm.next
|
629
|
+
fsm.current # => :red
|
630
|
+
```
|
631
|
+
|
632
|
+
Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
fsm = FiniteMachine.define do
|
636
|
+
initial :green
|
637
|
+
|
638
|
+
events {
|
639
|
+
event :next, from: :green do
|
640
|
+
choice :yellow, if: -> (context, a) { a < 1 }
|
641
|
+
choice :red, if: -> (context, a) { a > 1 }
|
642
|
+
default :red
|
643
|
+
end
|
644
|
+
}
|
645
|
+
end
|
646
|
+
|
647
|
+
fsm.current # => :green
|
648
|
+
fsm.next(0)
|
649
|
+
fsm.current # => :yellow
|
650
|
+
```
|
651
|
+
|
652
|
+
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.
|
653
|
+
|
654
|
+
## 5 Callbacks
|
547
655
|
|
548
656
|
You can watch state machine events and the information they provide by registering a callback. The following 5 types of callbacks are available in **FiniteMachine**:
|
549
657
|
|
@@ -578,27 +686,27 @@ fm.ready(1, 2, 3)
|
|
578
686
|
fm.go('Piotr!')
|
579
687
|
```
|
580
688
|
|
581
|
-
###
|
689
|
+
### 5.1 on_enter
|
582
690
|
|
583
691
|
The `on_enter` callback is executed before given state change is fired. By passing state name you can narrow down the listener to only watch out for enter state changes. Otherwise, all enter state changes will be watched.
|
584
692
|
|
585
|
-
###
|
693
|
+
### 5.2 on_transition
|
586
694
|
|
587
695
|
The `on_transition` callback is executed when given state change happens. By passing state name you can narrow down the listener to only watch out for transition state changes. Otherwise, all transition state changes will be watched.
|
588
696
|
|
589
|
-
###
|
697
|
+
### 5.3 on_exit
|
590
698
|
|
591
699
|
The `on_exit` callback is executed after a given state change happens. By passing state name you can narrow down the listener to only watch out for exit state changes. Otherwise, all exit state changes will be watched.
|
592
700
|
|
593
|
-
###
|
701
|
+
### 5.4 on_before
|
594
702
|
|
595
703
|
The `on_before` callback is executed before a given event happens. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
|
596
704
|
|
597
|
-
###
|
705
|
+
### 5.5 on_after
|
598
706
|
|
599
707
|
This callback is executed after a given event happened. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
|
600
708
|
|
601
|
-
###
|
709
|
+
### 5.6 once_on
|
602
710
|
|
603
711
|
**FiniteMachine** allows you to listen on initial state change or when the event is fired first time by using the following 5 types of callbacks:
|
604
712
|
|
@@ -608,7 +716,7 @@ This callback is executed after a given event happened. By default it will liste
|
|
608
716
|
* `once_before`
|
609
717
|
* `once_after`
|
610
718
|
|
611
|
-
###
|
719
|
+
### 5.7 Execution sequence
|
612
720
|
|
613
721
|
Assuming we have the following event specified:
|
614
722
|
|
@@ -629,7 +737,7 @@ then by calling `go` event the following callbacks in the following sequence wil
|
|
629
737
|
* `on_after :go` - callback after the `go` event
|
630
738
|
* `on_after` - generic callback after `any` event
|
631
739
|
|
632
|
-
###
|
740
|
+
### 5.8 Parameters
|
633
741
|
|
634
742
|
All callbacks get the `TransitionEvent` object with the following attributes.
|
635
743
|
|
@@ -657,7 +765,7 @@ end
|
|
657
765
|
fm.ready(3) # => 'lights switching from red to yellow in 3 seconds'
|
658
766
|
```
|
659
767
|
|
660
|
-
###
|
768
|
+
### 5.9 Same kind of callbacks
|
661
769
|
|
662
770
|
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
663
771
|
|
@@ -677,7 +785,7 @@ end
|
|
677
785
|
fm.slow # => will invoke both callbacks
|
678
786
|
```
|
679
787
|
|
680
|
-
###
|
788
|
+
### 5.10 Fluid callbacks
|
681
789
|
|
682
790
|
Callbacks can also be specified as full method calls.
|
683
791
|
|
@@ -699,7 +807,7 @@ fm = FiniteMachine.define do
|
|
699
807
|
end
|
700
808
|
```
|
701
809
|
|
702
|
-
###
|
810
|
+
### 5.11 Executing methods inside callbacks
|
703
811
|
|
704
812
|
In order to execute method from another object use `target` helper.
|
705
813
|
|
@@ -756,9 +864,9 @@ end
|
|
756
864
|
fm.back # => Go Piotr!
|
757
865
|
```
|
758
866
|
|
759
|
-
For more complex example see [Integration](#
|
867
|
+
For more complex example see [Integration](#7-integration) section.
|
760
868
|
|
761
|
-
###
|
869
|
+
### 5.12 Defining callbacks
|
762
870
|
|
763
871
|
When defining callbacks you are not limited to the `callbacks` helper. After **FiniteMachine** instance is created 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.
|
764
872
|
|
@@ -778,7 +886,7 @@ fm.on_enter_yellow do |event|
|
|
778
886
|
end
|
779
887
|
```
|
780
888
|
|
781
|
-
###
|
889
|
+
### 5.13 Asynchronous callbacks
|
782
890
|
|
783
891
|
By default all callbacks are run synchronosuly. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
|
784
892
|
|
@@ -794,7 +902,7 @@ or
|
|
794
902
|
|
795
903
|
This will ensure that when the callback is fired it will run in seperate thread outside of the main execution thread.
|
796
904
|
|
797
|
-
###
|
905
|
+
### 5.14 Cancelling inside callbacks
|
798
906
|
|
799
907
|
Preferred way to handle cancelling transitions is to use [3 Conditional transitions](#3-conditional-transitions). However if the logic is more than one liner you can cancel the event, hence the transition by returning `FiniteMachine::CANCELLED` constant from the callback scope. The two ways you can affect the event are
|
800
908
|
|
@@ -821,7 +929,7 @@ fm.ready
|
|
821
929
|
fm.current # => :red
|
822
930
|
```
|
823
931
|
|
824
|
-
##
|
932
|
+
## 6 Errors
|
825
933
|
|
826
934
|
By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
|
827
935
|
|
@@ -851,7 +959,7 @@ fm = FiniteMachine.define do
|
|
851
959
|
end
|
852
960
|
```
|
853
961
|
|
854
|
-
###
|
962
|
+
### 6.1 Using target
|
855
963
|
|
856
964
|
You can pass an external context via `target` helper that will be the receiver for the handler. The handler method needs to take one argument that will be called with the exception.
|
857
965
|
|
@@ -878,7 +986,7 @@ fm = FiniteMachine.define do
|
|
878
986
|
end
|
879
987
|
```
|
880
988
|
|
881
|
-
##
|
989
|
+
## 7 Integration
|
882
990
|
|
883
991
|
Since **FiniteMachine** is an object in its own right it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into exisiting classes.
|
884
992
|
|
@@ -926,7 +1034,7 @@ class Car
|
|
926
1034
|
end
|
927
1035
|
```
|
928
1036
|
|
929
|
-
###
|
1037
|
+
### 7.1 ActiveRecord
|
930
1038
|
|
931
1039
|
In order to integrate **FiniteMachine** with ActiveRecord use the `target` helper to reference the current class and call ActiveRecord methods inside the callbacks to persist the state.
|
932
1040
|
|
@@ -968,7 +1076,7 @@ account.manage.authorize
|
|
968
1076
|
account.state # => :access
|
969
1077
|
```
|
970
1078
|
|
971
|
-
##
|
1079
|
+
## 8 Tips
|
972
1080
|
|
973
1081
|
Creating a standalone **FiniteMachine** brings a number of benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself. Ideally, you would test the machine in isolation and then integrate it with other objects or ORMs.
|
974
1082
|
|
data/lib/finite_machine.rb
CHANGED
@@ -10,6 +10,7 @@ require "finite_machine/safety"
|
|
10
10
|
require "finite_machine/thread_context"
|
11
11
|
require "finite_machine/callable"
|
12
12
|
require "finite_machine/catchable"
|
13
|
+
require "finite_machine/choice_merger"
|
13
14
|
require "finite_machine/async_proxy"
|
14
15
|
require "finite_machine/async_call"
|
15
16
|
require "finite_machine/hook_event"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FiniteMachine
|
4
|
+
# A class responsible for merging choice options
|
5
|
+
class ChoiceMerger
|
6
|
+
include Threadable
|
7
|
+
|
8
|
+
# The context where choice is executed
|
9
|
+
attr_threadsafe :context
|
10
|
+
|
11
|
+
# The options passed in to the context
|
12
|
+
attr_threadsafe :options
|
13
|
+
|
14
|
+
# Initialize a ChoiceMerger
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def initialize(context, options)
|
18
|
+
self.context = context
|
19
|
+
self.options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create choice transition
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# event from: :green do
|
26
|
+
# choice :yellow
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @param [Symbol] to
|
30
|
+
# the to state
|
31
|
+
# @param [Hash] attrs
|
32
|
+
#
|
33
|
+
# @return [FiniteMachine::Transition]
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def choice(to, attrs = {})
|
37
|
+
opts = options.dup
|
38
|
+
opts.merge!(attrs)
|
39
|
+
opts.merge!(parsed_states: { options[:from] => to })
|
40
|
+
Transition.create(context.machine, opts)
|
41
|
+
end
|
42
|
+
alias_method :default, :choice
|
43
|
+
end # ChoiceMerger
|
44
|
+
end # FiniteMachine
|