finite_machine 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -2
- data/CHANGELOG.md +12 -0
- data/README.md +345 -105
- data/finite_machine.gemspec +2 -2
- data/lib/finite_machine.rb +12 -0
- data/lib/finite_machine/callable.rb +2 -7
- data/lib/finite_machine/catchable.rb +112 -0
- data/lib/finite_machine/dsl.rb +59 -44
- data/lib/finite_machine/hooks.rb +43 -0
- data/lib/finite_machine/observer.rb +39 -40
- data/lib/finite_machine/state_machine.rb +37 -18
- data/lib/finite_machine/subscribers.rb +1 -1
- data/lib/finite_machine/threadable.rb +8 -2
- data/lib/finite_machine/transition.rb +45 -1
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/unit/callable/call_spec.rb +91 -0
- data/spec/unit/callbacks_spec.rb +96 -12
- data/spec/unit/events_spec.rb +1 -1
- data/spec/unit/handlers_spec.rb +99 -0
- data/spec/unit/if_unless_spec.rb +3 -3
- data/spec/unit/initialize_spec.rb +40 -0
- data/spec/unit/target_spec.rb +137 -0
- data/spec/unit/transition/parse_states_spec.rb +16 -5
- metadata +15 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88cd408cda756703b6665126580605500ad9273f
|
4
|
+
data.tar.gz: 93a8062b97cd5a0fed0367c091cbb06e45242b2c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb7c16c1ec7e7238cfb5d2b37ab554741733b4957af9855878a0b35d7ef4e04f334542c8889d5829ed9d002ac9ef847c20f093590af44db7310ced42b33e53b1
|
7
|
+
data.tar.gz: 2963f9d98146612ae2a6d9c3ba5a690bb06d1f32f8fd6e1da1b817f8fdcb0923ec371f38ae48ce4e92ee553c65214f3a1dc34d095d523431a302b32b377008b9
|
data/.travis.yml
CHANGED
@@ -6,13 +6,17 @@ rvm:
|
|
6
6
|
- 2.0.0
|
7
7
|
- 2.1.0
|
8
8
|
- ruby-head
|
9
|
-
- rbx
|
10
9
|
matrix:
|
11
10
|
include:
|
12
11
|
- rvm: jruby-19mode
|
13
12
|
- rvm: jruby-20mode
|
14
13
|
- rvm: jruby-21mode
|
15
14
|
- rvm: jruby-head
|
15
|
+
- rvm: rbx
|
16
16
|
allow_failures:
|
17
|
-
- rvm:
|
17
|
+
- rvm: ruby-head
|
18
|
+
- rvm: jruby-head
|
19
|
+
- rvm: rbx
|
18
20
|
fast_finish: true
|
21
|
+
branches:
|
22
|
+
only: master
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
0.2.0 (March 01, 2014)
|
2
|
+
|
3
|
+
* Ensure correct transition object state
|
4
|
+
* Add methods synchronization for thread safety
|
5
|
+
* Fix bug - callback event object returns correct from state
|
6
|
+
* Add ability to define custom initial event
|
7
|
+
* Add hooks class for callbacks registration
|
8
|
+
* Extend threadable accessors
|
9
|
+
* Add generic state and event listeners
|
10
|
+
* Add target to allow integration with external objects,
|
11
|
+
and allow easy method lookup through callback context
|
12
|
+
* Add ability to specify custom handlers for error conditions
|
data/README.md
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# FiniteMachine
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/finite_machine.png)][gem]
|
3
|
+
[![Build Status](https://secure.travis-ci.org/peter-murach/finite_machine.png?branch=master)][travis]
|
4
|
+
[![Code Climate](https://codeclimate.com/github/peter-murach/finite_machine.png)][codeclimate]
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
[gem]: http://badge.fury.io/rb/finite_machine
|
7
|
+
[travis]: http://travis-ci.org/peter-murach/finite_machine
|
8
|
+
[codeclimate]: https://codeclimate.com/github/peter-murach/finite_machine
|
9
|
+
|
10
|
+
A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and add callbacks that can be triggered synchronously or asynchronously.
|
6
11
|
|
7
12
|
## Features
|
8
13
|
|
@@ -22,7 +27,7 @@ Add this line to your application's Gemfile:
|
|
22
27
|
|
23
28
|
gem 'finite_machine'
|
24
29
|
|
25
|
-
|
30
|
+
Then execute:
|
26
31
|
|
27
32
|
$ bundle
|
28
33
|
|
@@ -30,6 +35,16 @@ Or install it yourself as:
|
|
30
35
|
|
31
36
|
$ gem install finite_machine
|
32
37
|
|
38
|
+
## Contents
|
39
|
+
|
40
|
+
* [1. Usage](#1-usage)
|
41
|
+
* [2. Transitions](#2-transitions)
|
42
|
+
* [3. Conditional transitions](#3-conditional-transitions)
|
43
|
+
* [4. Callbacks](#4-callbacks)
|
44
|
+
* [5. Errors](#5-errors)
|
45
|
+
* [6. Integration](#6-integration)
|
46
|
+
* [7. Tips](#7-tips)
|
47
|
+
|
33
48
|
## 1 Usage
|
34
49
|
|
35
50
|
Here is a very simple example of a state machine:
|
@@ -52,11 +67,11 @@ fm = FiniteMachine.define do
|
|
52
67
|
end
|
53
68
|
```
|
54
69
|
|
55
|
-
As the example demonstrates, by calling the `define` method on **FiniteMachine**
|
70
|
+
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](#4-callbacks) sections for more details.
|
56
71
|
|
57
72
|
### 1.1 current
|
58
73
|
|
59
|
-
The **FiniteMachine** allows to query the current state by calling `current` method.
|
74
|
+
The **FiniteMachine** allows you to query the current state by calling the `current` method.
|
60
75
|
|
61
76
|
```ruby
|
62
77
|
fm.current # => :red
|
@@ -64,105 +79,120 @@ The **FiniteMachine** allows to query the current state by calling `current` met
|
|
64
79
|
|
65
80
|
### 1.2 initial
|
66
81
|
|
67
|
-
There are number of ways to provide initial state **FiniteMachine** depending on your requirements.
|
82
|
+
There are number of ways to provide the initial state **FiniteMachine** depending on your requirements.
|
68
83
|
|
69
|
-
By default the **FiniteMachine** will be in `:none` state and you
|
84
|
+
By default the **FiniteMachine** will be in the `:none` state and you will need to provide an event to transition out of this state.
|
70
85
|
|
71
86
|
```ruby
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
87
|
+
fm = FiniteMachine.define do
|
88
|
+
events {
|
89
|
+
event :start, :none => :green
|
90
|
+
event :slow, :green => :yellow
|
91
|
+
event :stop, :yellow => :red
|
92
|
+
}
|
93
|
+
end
|
79
94
|
|
80
|
-
|
95
|
+
fm.current # => :none
|
81
96
|
```
|
82
97
|
|
83
|
-
If you specify initial state using `initial` helper
|
98
|
+
If you specify initial state using the `initial` helper, an `init` event will be created and triggered when the state machine is created.
|
84
99
|
|
85
100
|
```ruby
|
86
|
-
|
87
|
-
|
101
|
+
fm = FiniteMachine.define do
|
102
|
+
initial :green
|
88
103
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
104
|
+
events {
|
105
|
+
event :slow, :green => :yellow
|
106
|
+
event :stop, :yellow => :red
|
107
|
+
}
|
108
|
+
end
|
94
109
|
|
95
|
-
|
110
|
+
fm.current # => :green
|
96
111
|
```
|
97
112
|
|
98
|
-
|
113
|
+
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.
|
99
114
|
|
100
115
|
```ruby
|
101
|
-
|
102
|
-
|
116
|
+
fm = FiniteMachine.define do
|
117
|
+
initial :green, event: :start
|
103
118
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
119
|
+
events {
|
120
|
+
event :slow, :green => :yellow
|
121
|
+
event :stop, :yellow => :red
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
fm.current # => :green
|
126
|
+
```
|
127
|
+
|
128
|
+
If you want to defer calling the initial state method pass the `:defer` option to the `initial` helper.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
fm = FiniteMachine.define do
|
132
|
+
initial state: :green, defer: true
|
133
|
+
|
134
|
+
events {
|
135
|
+
event :slow, :green => :yellow
|
136
|
+
event :stop, :yellow => :red
|
137
|
+
}
|
138
|
+
end
|
139
|
+
fm.current # => :none
|
140
|
+
fm.init
|
141
|
+
fm.current # => :green
|
112
142
|
```
|
113
143
|
|
114
144
|
### 1.3 terminal
|
115
145
|
|
116
|
-
To specify a final state **FiniteMachine** uses `terminal` method.
|
146
|
+
To specify a final state **FiniteMachine** uses the `terminal` method.
|
117
147
|
|
118
148
|
```ruby
|
119
|
-
|
120
|
-
|
121
|
-
|
149
|
+
fm = FiniteMachine.define do
|
150
|
+
initial :green
|
151
|
+
terminal :red
|
122
152
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
153
|
+
events {
|
154
|
+
event :slow, :green => :yellow
|
155
|
+
event :stop, :yellow => :red
|
156
|
+
}
|
157
|
+
end
|
128
158
|
```
|
129
159
|
|
130
|
-
|
160
|
+
When the terminal state has been specified, you can use `finished?` method on the state machine instance to verify if the terminal state has been reached or not.
|
131
161
|
|
132
162
|
```ruby
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
163
|
+
fm.finished? # => false
|
164
|
+
fm.slow
|
165
|
+
fm.finished? # => false
|
166
|
+
fm.stop
|
167
|
+
fm.finished? # => true
|
138
168
|
```
|
139
169
|
|
140
170
|
### 1.4 is?
|
141
171
|
|
142
|
-
To verify whether or not a state machine is in a given state, **FiniteMachine** uses `is?` method. It returns `true` if
|
172
|
+
To verify whether or not a state machine is in a given state, **FiniteMachine** uses `is?` method. It returns `true` if the machine is found to be in the given state, or `false` otherwise.
|
143
173
|
|
144
174
|
```ruby
|
145
|
-
|
146
|
-
|
175
|
+
fm.is?(:red) # => true
|
176
|
+
fm.is?(:yellow) # => false
|
147
177
|
```
|
148
178
|
|
149
179
|
### 1.5 can? and cannot?
|
150
180
|
|
151
|
-
To verify whether or not an event can be fired, **FiniteMachine** provides `can?` or `cannot?` methods. `can?` checks if
|
181
|
+
To verify whether or not an event can be fired, **FiniteMachine** provides `can?` or `cannot?` methods. `can?` checks if **FiniteMachine** can fire a given event, returning true, otherwise, it will return false. `cannot?` is simply the inverse of `can?`.
|
152
182
|
|
153
183
|
```ruby
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
184
|
+
fm.can?(:ready) # => true
|
185
|
+
fm.can?(:go) # => false
|
186
|
+
fm.cannot?(:ready) # => false
|
187
|
+
fm.cannot?(:go) # => true
|
158
188
|
```
|
159
189
|
|
160
190
|
### 1.6 states
|
161
191
|
|
162
|
-
You can use `states` method to
|
192
|
+
You can use the `states` method to return an array of all the states for a given state machine.
|
163
193
|
|
164
194
|
```ruby
|
165
|
-
|
195
|
+
fm.states # => [:none, :green, :yellow, :red]
|
166
196
|
```
|
167
197
|
|
168
198
|
### 1.7 target
|
@@ -170,20 +200,43 @@ You can use `states` method to query for all states. It returns an array of all
|
|
170
200
|
If you need to execute some external code in the context of the current state machine use `target` helper.
|
171
201
|
|
172
202
|
```ruby
|
173
|
-
|
203
|
+
car = Car.new
|
174
204
|
|
175
|
-
|
176
|
-
|
205
|
+
fm = FiniteMachine.define do
|
206
|
+
initial :neutral
|
177
207
|
|
178
|
-
|
208
|
+
target car
|
179
209
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
210
|
+
events {
|
211
|
+
event :start, :neutral => :one, if: "engine_on?"
|
212
|
+
event :shift, :one => :two
|
213
|
+
}
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
Furthermore, the context created through `target` helper will allow you to reference and call methods from another object.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
car = Car.new
|
221
|
+
|
222
|
+
fm = FiniteMachine.define do
|
223
|
+
initial :neutral
|
224
|
+
|
225
|
+
target car
|
226
|
+
|
227
|
+
events {
|
228
|
+
event :start, :neutral => :one, if: "engine_on?"
|
229
|
+
}
|
230
|
+
|
231
|
+
callbacks {
|
232
|
+
on_enter_start do |event| turn_engine_on end
|
233
|
+
on_exit_start do |event| turn_engine_off end
|
234
|
+
}
|
235
|
+
end
|
185
236
|
```
|
186
237
|
|
238
|
+
For more complex example see [Integration](#6-integration) section.
|
239
|
+
|
187
240
|
## 2 Transitions
|
188
241
|
|
189
242
|
The `events` scope exposes the `event` helper to define possible state transitions.
|
@@ -198,7 +251,7 @@ in the form of `:from` and `:to` hash keys or by using the state names themselve
|
|
198
251
|
event :start, :neutral => :first
|
199
252
|
```
|
200
253
|
|
201
|
-
Once specified the **FiniteMachine** will create custom methods for transitioning between
|
254
|
+
Once specified, the **FiniteMachine** will create custom methods for transitioning between each state.
|
202
255
|
The following methods trigger transitions for the example state machine.
|
203
256
|
|
204
257
|
* ready
|
@@ -207,14 +260,14 @@ The following methods trigger transitions for the example state machine.
|
|
207
260
|
|
208
261
|
### 2.1 Performing transitions
|
209
262
|
|
210
|
-
In order to transition to the next reachable state simply call the event name on the **FiniteMachine** instance.
|
263
|
+
In order to transition to the next reachable state, simply call the event's name on the **FiniteMachine** instance.
|
211
264
|
|
212
265
|
```ruby
|
213
266
|
fm.ready
|
214
267
|
fm.current # => :yellow
|
215
268
|
```
|
216
269
|
|
217
|
-
|
270
|
+
Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback.
|
218
271
|
|
219
272
|
```ruby
|
220
273
|
fm.go('Piotr!')
|
@@ -278,7 +331,7 @@ You can also execute methods on an associated object by passing it as an argumen
|
|
278
331
|
end
|
279
332
|
|
280
333
|
car = Car.new
|
281
|
-
car.
|
334
|
+
car.turn_engine_on
|
282
335
|
|
283
336
|
fm = FiniteMachine.define do
|
284
337
|
initial :neutral
|
@@ -310,7 +363,7 @@ You can also use a symbol corresponding to the name of a method that will get ca
|
|
310
363
|
end
|
311
364
|
```
|
312
365
|
|
313
|
-
### 3.
|
366
|
+
### 3.3 Using a String
|
314
367
|
|
315
368
|
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.
|
316
369
|
|
@@ -328,7 +381,7 @@ Finally, it's possible to use string that will be evaluated using `eval` and nee
|
|
328
381
|
|
329
382
|
### 3.4 Combining transition conditions
|
330
383
|
|
331
|
-
When multiple conditions define whether or not a transition should happen, an Array can be used.
|
384
|
+
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.
|
332
385
|
|
333
386
|
```ruby
|
334
387
|
fsm = FiniteMachine.define do
|
@@ -347,15 +400,24 @@ The transition only runs when all the `:if` conditions and none of the `unless`
|
|
347
400
|
|
348
401
|
## 4 Callbacks
|
349
402
|
|
350
|
-
You can
|
403
|
+
You can watch state machine events and the information they provide by registering a callback. The following 3 types of callbacks are available in **FiniteMachine**:
|
351
404
|
|
352
405
|
* `on_enter`
|
353
406
|
* `on_transition`
|
354
407
|
* `on_exit`
|
355
408
|
|
356
|
-
|
409
|
+
In addition, you can listen for generic state changes or events fired by using the following 6 callbacks:
|
357
410
|
|
358
|
-
|
411
|
+
* `on_enter_state`
|
412
|
+
* `on_enter_event`
|
413
|
+
* `on_transition_state`
|
414
|
+
* `on_transition_event`
|
415
|
+
* `on_exit_state`
|
416
|
+
* `on_exit_event`
|
417
|
+
|
418
|
+
Use the `callbacks` scope to introduce the listeners. You can register a callback to listen for state changes or events being triggered. Use the state or event name as a first parameter to the callback followed by a list arguments that you expect to receive.
|
419
|
+
|
420
|
+
When you subscribe to the `:green` state event, the callback will be called whenever someone instruments change for that state. The same will happend on subscription to event `ready`, namely, the callback will be called each time the state transition method is called.
|
359
421
|
|
360
422
|
```ruby
|
361
423
|
fm = FiniteMachine.define do
|
@@ -380,19 +442,25 @@ fm.go('Piotr!')
|
|
380
442
|
|
381
443
|
### 4.1 on_enter
|
382
444
|
|
383
|
-
This method is executed before given event or state change
|
445
|
+
This method is executed before given event or state change. If you provide only a callback without the name of the state or event to listen out for, then `:any` state and `:any` event will be observered.
|
446
|
+
|
447
|
+
You can further narrow down the listener to only watch enter state changes using `on_enter_state` callback. Similarly, use `on_enter_event` to only watch for event changes.
|
384
448
|
|
385
449
|
### 4.2 on_transition
|
386
450
|
|
387
|
-
This method is executed when given event or state change happens. If you provide only a callback without the name
|
451
|
+
This method is executed when given event or state change happens. If you provide only a callback without the name of the state or event to listen out for, then `:any` state and `:any` event will be observered.
|
452
|
+
|
453
|
+
You can further narrow down the listener to only watch state transition changes using `on_transition_state` callback. Similarly, use `on_transition_event` to only watch for event transition changes.
|
388
454
|
|
389
455
|
### 4.3 on_exit
|
390
456
|
|
391
|
-
This method is executed after given event or state change happens. If you provide only a callback without the name
|
457
|
+
This method is executed after a given event or state change happens. If you provide only a callback without the name of the state or event to listen for, then `:any` state and `:any` event will be observered.
|
458
|
+
|
459
|
+
You can further narrow down the listener to only watch state exit changes using `on_exit_state` callback. Similarly, use `on_exit_event` to only watch for event exit changes.
|
392
460
|
|
393
461
|
### 4.4 Parameters
|
394
462
|
|
395
|
-
All callbacks
|
463
|
+
All callbacks get the `TransitionEvent` object with the following attributes.
|
396
464
|
|
397
465
|
* name # the event name
|
398
466
|
* from # the state transitioning from
|
@@ -400,24 +468,42 @@ All callbacks are passed `TransitionEvent` object with the following attributes.
|
|
400
468
|
|
401
469
|
followed by the rest of arguments that were passed to the event method.
|
402
470
|
|
471
|
+
```ruby
|
472
|
+
fm = FiniteMachine.define do
|
473
|
+
initial :red
|
474
|
+
|
475
|
+
events {
|
476
|
+
event :ready, :red => :yellow
|
477
|
+
}
|
478
|
+
|
479
|
+
callbacks {
|
480
|
+
on_enter_ready { |event, time|
|
481
|
+
puts "lights switching from #{event.from} to #{event.to} in #{time} seconds"
|
482
|
+
}
|
483
|
+
}
|
484
|
+
end
|
485
|
+
|
486
|
+
fm.ready(3) # => 'lights switching from red to yellow in 3 seconds'
|
487
|
+
```
|
488
|
+
|
403
489
|
### 4.5 Same kind of callbacks
|
404
490
|
|
405
491
|
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
406
492
|
|
407
493
|
```ruby
|
408
|
-
|
409
|
-
|
494
|
+
fm = FiniteMachine.define do
|
495
|
+
initial :green
|
410
496
|
|
411
|
-
|
412
|
-
|
413
|
-
|
497
|
+
events {
|
498
|
+
event :slow, :green => :yellow
|
499
|
+
}
|
414
500
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
501
|
+
callbacks {
|
502
|
+
on_enter(:yellow) { this_is_run_first }
|
503
|
+
on_enter(:yellow) { then_this }
|
504
|
+
}
|
505
|
+
end
|
506
|
+
fm.slow # => will invoke both callbacks
|
421
507
|
```
|
422
508
|
|
423
509
|
### 4.6 Fluid callbacks
|
@@ -442,30 +528,143 @@ fm = FiniteMachine.define do
|
|
442
528
|
end
|
443
529
|
```
|
444
530
|
|
531
|
+
### 4.7 Executing methods inside callbacks
|
532
|
+
|
533
|
+
In order to execute method from another object use `target` helper.
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
class Car
|
537
|
+
attr_accessor :reverse_lights
|
538
|
+
|
539
|
+
def turn_reverse_lights_off
|
540
|
+
@reverse_lights = false
|
541
|
+
end
|
542
|
+
|
543
|
+
def turn_reverse_lights_on
|
544
|
+
@reverse_lights = true
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
car = Car.new
|
549
|
+
|
550
|
+
fm = FiniteMachine.define do
|
551
|
+
initial :neutral
|
552
|
+
|
553
|
+
target car
|
554
|
+
|
555
|
+
events {
|
556
|
+
event :forward, [:reverse, :neutral] => :one
|
557
|
+
event :back, [:neutral, :one] => :reverse
|
558
|
+
}
|
559
|
+
|
560
|
+
callbacks {
|
561
|
+
on_enter_reverse { |event| turn_reverse_lights_on }
|
562
|
+
on_exit_reverse { |event| turn_reverse_lights_off }
|
563
|
+
}
|
564
|
+
end
|
565
|
+
```
|
566
|
+
|
567
|
+
Note that you can also fire events from callbacks.
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
fm = FiniteMachine.define do
|
571
|
+
initial :neutral
|
572
|
+
|
573
|
+
target car
|
574
|
+
events {
|
575
|
+
event :forward, [:reverse, :neutral] => :one
|
576
|
+
event :back, [:neutral, :one] => :reverse
|
577
|
+
}
|
578
|
+
|
579
|
+
callbacks {
|
580
|
+
on_enter_reverse { |event| forward('Piotr!') }
|
581
|
+
on_exit_reverse { |event, name| puts "Go #{name}" }
|
582
|
+
}
|
583
|
+
end
|
584
|
+
fm.back # => Go Piotr!
|
585
|
+
```
|
586
|
+
|
587
|
+
For more complex example see [Integration](#6-integration) section.
|
588
|
+
|
445
589
|
## 5 Errors
|
446
590
|
|
447
|
-
|
591
|
+
By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
|
448
592
|
|
449
|
-
|
593
|
+
* FiniteMachine::TransitionError
|
594
|
+
* FiniteMachine::InvalidStateError
|
595
|
+
* FiniteMachine::InvalidCallbackError
|
596
|
+
|
597
|
+
You can attach specific error handler inside the `handlers` scope by passing the name of the error and actual callback to be executed when the error happens inside the `handle` method. The `handle` receives a list of exception class or exception class names, and an option `:with` with a name of the method or a Proc object to be called to handle the error. As an alternative, you can pass a block.
|
450
598
|
|
451
599
|
```ruby
|
600
|
+
fm = FiniteMachine.define do
|
601
|
+
initial :green, event: :start
|
602
|
+
|
603
|
+
events {
|
604
|
+
event :slow, :green => :yellow
|
605
|
+
event :stop, :yellow => :red
|
606
|
+
}
|
607
|
+
|
608
|
+
handlers {
|
609
|
+
handle FiniteMachine::InvalidStateError do |exception|
|
610
|
+
# run some custom logging
|
611
|
+
raise exception
|
612
|
+
end
|
613
|
+
|
614
|
+
handle FiniteMachine::TransitionError, with: proc { |exception| ... }
|
615
|
+
}
|
616
|
+
end
|
617
|
+
```
|
618
|
+
|
619
|
+
### 5.1 Using target
|
620
|
+
|
621
|
+
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.
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
class Logger
|
625
|
+
def log_error(exception)
|
626
|
+
puts "Exception : #{exception.message}"
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
fm = FiniteMachine.define do
|
631
|
+
target logger
|
632
|
+
|
633
|
+
initial :green
|
634
|
+
|
635
|
+
events {
|
636
|
+
event :slow, :green => :yellow
|
637
|
+
event :stop, :yellow => :red
|
638
|
+
}
|
639
|
+
|
640
|
+
handlers {
|
641
|
+
handle 'InvalidStateError', with: :log_error
|
642
|
+
}
|
643
|
+
end
|
644
|
+
```
|
645
|
+
|
646
|
+
## 6 Integration
|
452
647
|
|
648
|
+
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.
|
649
|
+
|
650
|
+
```ruby
|
453
651
|
class Car
|
454
652
|
attr_accessor :reverse_lights
|
455
653
|
|
456
654
|
def turn_reverse_lights_off
|
457
|
-
reverse_lights = false
|
655
|
+
@reverse_lights = false
|
458
656
|
end
|
459
657
|
|
460
658
|
def turn_reverse_lights_on
|
461
|
-
reverse_lights = true
|
659
|
+
@reverse_lights = true
|
462
660
|
end
|
463
661
|
|
464
662
|
def gears
|
663
|
+
context = self
|
465
664
|
@gears ||= FiniteMachine.define do
|
466
665
|
initial :neutral
|
467
666
|
|
468
|
-
target:
|
667
|
+
target: context
|
469
668
|
|
470
669
|
events {
|
471
670
|
event :start, :neutral => :one
|
@@ -475,27 +674,68 @@ class Car
|
|
475
674
|
}
|
476
675
|
|
477
676
|
callbacks {
|
478
|
-
on_enter :reverse do |
|
479
|
-
|
677
|
+
on_enter :reverse do |event|
|
678
|
+
turn_reverse_lights_on
|
480
679
|
end
|
481
680
|
|
482
|
-
on_exit :reverse do |
|
483
|
-
|
681
|
+
on_exit :reverse do |event|
|
682
|
+
turn_reverse_lights_off
|
484
683
|
end
|
485
684
|
|
486
|
-
on_transition do |
|
685
|
+
on_transition do |event|
|
487
686
|
puts "shifted from #{event.from} to #{event.to}"
|
488
687
|
end
|
489
688
|
}
|
490
689
|
end
|
491
690
|
end
|
492
691
|
end
|
692
|
+
```
|
693
|
+
|
694
|
+
### 6.1 ActiveRecord
|
695
|
+
|
696
|
+
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.
|
697
|
+
|
698
|
+
```ruby
|
699
|
+
class Account < ActiveRecord::Base
|
700
|
+
validates :state, presence: true
|
701
|
+
|
702
|
+
def initialize
|
703
|
+
self.state = :unapproved
|
704
|
+
end
|
705
|
+
|
706
|
+
def manage
|
707
|
+
context = self
|
708
|
+
@machine ||= FiniteMachine.define do
|
709
|
+
target context
|
710
|
+
|
711
|
+
initial context.state
|
712
|
+
|
713
|
+
events {
|
714
|
+
event :enqueue, :unapproved => :pending
|
715
|
+
event :authorize, :pending => :access
|
716
|
+
}
|
717
|
+
|
718
|
+
callbacks {
|
719
|
+
on_enter_state do |event|
|
720
|
+
state = event.to
|
721
|
+
save
|
722
|
+
end
|
723
|
+
}
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
493
727
|
|
728
|
+
account = Account.new
|
729
|
+
account.state # => :unapproved
|
730
|
+
account.manage.enqueue
|
731
|
+
account.state # => :pending
|
732
|
+
account.manage.authorize
|
733
|
+
account.state # => :access
|
494
734
|
```
|
495
735
|
|
496
736
|
## 7 Tips
|
497
737
|
|
498
|
-
Creating standalone **FiniteMachine** brings few benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself.
|
738
|
+
Creating a standalone **FiniteMachine** brings few 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.
|
499
739
|
|
500
740
|
## Contributing
|
501
741
|
|