finite_machine 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/README.md +467 -11
- data/Rakefile +40 -0
- data/lib/finite_machine.rb +44 -2
- data/lib/finite_machine/callable.rb +51 -0
- data/lib/finite_machine/dsl.rb +134 -0
- data/lib/finite_machine/event.rb +66 -0
- data/lib/finite_machine/observer.rb +136 -0
- data/lib/finite_machine/state_machine.rb +188 -0
- data/lib/finite_machine/subscribers.rb +41 -0
- data/lib/finite_machine/threadable.rb +44 -0
- data/lib/finite_machine/transition.rb +69 -0
- data/lib/finite_machine/version.rb +3 -1
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/callbacks_spec.rb +391 -0
- data/spec/unit/can_spec.rb +50 -0
- data/spec/unit/define_spec.rb +32 -0
- data/spec/unit/events_spec.rb +256 -0
- data/spec/unit/finished_spec.rb +51 -0
- data/spec/unit/if_unless_spec.rb +196 -0
- data/spec/unit/initialize_spec.rb +59 -0
- data/spec/unit/is_spec.rb +33 -0
- data/spec/unit/states_spec.rb +21 -0
- data/spec/unit/transition/parse_states_spec.rb +34 -0
- metadata +35 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7582fc3410fbb09056dee6ed167b505ebe6febaf
|
4
|
+
data.tar.gz: 3d6336949ace9e8ce3ef3c469346ae6ea1c69044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1e6145d24909b4762a5be15a85f85c8eac2a07239d3f350ac1eba4283bfe9d4dd517fada486f4b749b97234d32c5b9957b82ccf8bc1ba57441f67a9ba6957d4
|
7
|
+
data.tar.gz: 0c948fa0384294b6ead78ae12b6e367d033bd6adf69be4c32420e4ac67cff4a07f565f1870c9d8f62653ccdce51c606cc9f828572e5dcf15ce5d471c80ec6438
|
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
bundler_args: --without yard guard benchmarks
|
3
|
+
script: "bundle exec rake ci"
|
4
|
+
rvm:
|
5
|
+
- 1.9.3
|
6
|
+
- 2.0.0
|
7
|
+
- 2.1.0
|
8
|
+
- ruby-head
|
9
|
+
- rbx
|
10
|
+
matrix:
|
11
|
+
include:
|
12
|
+
- rvm: jruby-19mode
|
13
|
+
- rvm: jruby-20mode
|
14
|
+
- rvm: jruby-21mode
|
15
|
+
- rvm: jruby-head
|
16
|
+
allow_failures:
|
17
|
+
- rvm: 2.1.0
|
18
|
+
fast_finish: true
|
data/README.md
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
# FiniteMachine
|
2
2
|
|
3
|
-
A minimal finite state machine with a straightforward syntax.
|
3
|
+
A minimal finite state machine with a straightforward syntax. With intuitive
|
4
|
+
syntax you can quickly model states and add callbacks that can be triggered
|
5
|
+
synchronously or asynchronously.
|
4
6
|
|
5
7
|
## Features
|
6
8
|
|
9
|
+
* plain object state machine
|
10
|
+
* easy custom object integration
|
11
|
+
* natural DSL for declaring events, exceptions and callbacks
|
12
|
+
* observers (pub/sub) for state changes
|
13
|
+
* ability to check reachable states
|
14
|
+
* ability to check for terminal state
|
15
|
+
* conditional transitions
|
16
|
+
* sync and async callbacks (TODO - only sync)
|
17
|
+
* nested/composable states (TODO)
|
7
18
|
|
8
19
|
## Installation
|
9
20
|
|
@@ -19,27 +30,472 @@ Or install it yourself as:
|
|
19
30
|
|
20
31
|
$ gem install finite_machine
|
21
32
|
|
22
|
-
## Usage
|
33
|
+
## 1 Usage
|
23
34
|
|
35
|
+
Here is a very simple example of a state machine:
|
36
|
+
|
37
|
+
```ruby
|
24
38
|
fm = FiniteMachine.define do
|
25
|
-
initial
|
39
|
+
initial :red
|
26
40
|
|
27
41
|
events {
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
event :ready, :red => :yellow
|
43
|
+
event :go, :yellow => :green
|
44
|
+
event :stop, :green => :red
|
31
45
|
}
|
32
46
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
47
|
+
callbacks {
|
48
|
+
on_enter :ready { |event| ... }
|
49
|
+
on_enter :go { |event| ... }
|
50
|
+
on_enter :stop { |event| ... }
|
37
51
|
}
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
As the example demonstrates, by calling the `define` method on **FiniteMachine** one gets to create an instance of finite state machine. The `events` and `callbacks` scopes help to define the behaviour of the machine. Read [Transitions](#transitions) and [Callbacks](#callbacks) sections for more detail.
|
56
|
+
|
57
|
+
### 1.1 current
|
58
|
+
|
59
|
+
The **FiniteMachine** allows to query the current state by calling `current` method.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
fm.current # => :red
|
63
|
+
```
|
64
|
+
|
65
|
+
### 1.2 initial
|
66
|
+
|
67
|
+
There are number of ways to provide initial state **FiniteMachine** depending on your requirements.
|
68
|
+
|
69
|
+
By default the **FiniteMachine** will be in `:none` state and you would need to provide event to transition out of this state.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
fm = FiniteMachine.define do
|
73
|
+
events {
|
74
|
+
event :start, :none => :green
|
75
|
+
event :slow, :green => :yellow
|
76
|
+
event :stop, :yellow => :red
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
fm.current # => :none
|
81
|
+
```
|
82
|
+
|
83
|
+
If you specify initial state using `initial` helper then an `init` event will be created and triggered when the state machine is constructed.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
fm = FiniteMachine.define do
|
87
|
+
initial :green
|
88
|
+
|
89
|
+
events {
|
90
|
+
event :slow, :green => :yellow
|
91
|
+
event :stop, :yellow => :red
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
fm.current # => :green
|
96
|
+
```
|
97
|
+
|
98
|
+
Finally, if you want to defer calling the initial state method pass the `:defer` option to `initial` helper.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
fm = FiniteMachine.define do
|
102
|
+
initial state: :green, defer: true
|
103
|
+
|
104
|
+
events {
|
105
|
+
event :slow, :green => :yellow
|
106
|
+
event :stop, :yellow => :red
|
107
|
+
}
|
108
|
+
end
|
109
|
+
fm.current # => :none
|
110
|
+
fm.init
|
111
|
+
fm.current # => :green
|
112
|
+
```
|
113
|
+
|
114
|
+
### 1.3 terminal
|
115
|
+
|
116
|
+
To specify a final state **FiniteMachine** uses `terminal` method.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
fm = FiniteMachine.define do
|
120
|
+
initial :green
|
121
|
+
terminal :red
|
122
|
+
|
123
|
+
events {
|
124
|
+
event :slow, :green => :yellow
|
125
|
+
event :stop, :yellow => :red
|
126
|
+
}
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
After 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
|
+
|
132
|
+
```ruby
|
133
|
+
fm.finished? # => false
|
134
|
+
fm.slow
|
135
|
+
fm.finished? # => false
|
136
|
+
fm.stop
|
137
|
+
fm.finished? # => true
|
138
|
+
```
|
139
|
+
|
140
|
+
### 1.4 is?
|
141
|
+
|
142
|
+
To verify whether or not a state machine is in a given state, **FiniteMachine** uses `is?` method. It returns `true` if machien is found to be in a state, and `false` otherwise.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
fm.is?(:red) # => true
|
146
|
+
fm.is?(:yellow) # => false
|
147
|
+
```
|
148
|
+
|
149
|
+
### 1.5 can? and cannot?
|
150
|
+
|
151
|
+
To verify whether or not an event can be fired, **FiniteMachine** provides `can?` or `cannot?` methods. `can?` checks if transition can be performed and returns true if state change can happend, and false otherwise. `cannot?` is simply the inverse of `can?`.
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
fm.can?(:ready) # => true
|
155
|
+
fm.can?(:go) # => false
|
156
|
+
fm.cannot?(:ready) # => false
|
157
|
+
fm.cannot?(:go) # => true
|
158
|
+
```
|
159
|
+
|
160
|
+
### 1.6 states
|
161
|
+
|
162
|
+
You can use `states` method to query for all states. It returns an array of all the states for the current state machine.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
fm.states # => [:none, :green, :yellow, :red]
|
166
|
+
```
|
167
|
+
|
168
|
+
### 1.7 target
|
169
|
+
|
170
|
+
If you need to execute some external code in the context of the current state machine use `target` helper.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
car = Car.new
|
38
174
|
|
175
|
+
fm = FiniteMachine.define do
|
176
|
+
initial :neutral
|
177
|
+
|
178
|
+
target car
|
179
|
+
|
180
|
+
events {
|
181
|
+
event :start, :neutral => :one, if: "engine_on?"
|
182
|
+
event :shift, :one => :two
|
183
|
+
}
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
## 2 Transitions
|
188
|
+
|
189
|
+
The `events` scope exposes the `event` helper to define possible state transitions.
|
190
|
+
|
191
|
+
The `event` helper accepts as a first parameter the name which will later be used to create
|
192
|
+
method on the **FiniteMachine** instance. As a second parameter `event` accepts an arbitrary number of states either
|
193
|
+
in the form of `:from` and `:to` hash keys or by using the state names themselves as key value pairs.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
event :start, from: :neutral, to: :first
|
197
|
+
or
|
198
|
+
event :start, :neutral => :first
|
199
|
+
```
|
200
|
+
|
201
|
+
Once specified the **FiniteMachine** will create custom methods for transitioning between the states.
|
202
|
+
The following methods trigger transitions for the example state machine.
|
203
|
+
|
204
|
+
* ready
|
205
|
+
* go
|
206
|
+
* stop
|
207
|
+
|
208
|
+
### 2.1 Performing transitions
|
209
|
+
|
210
|
+
In order to transition to the next reachable state simply call the event name on the **FiniteMachine** instance.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
fm.ready
|
214
|
+
fm.current # => :yellow
|
215
|
+
```
|
216
|
+
|
217
|
+
Further, you can pass additional parameters with the method call that will be available in the triggered callback.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
fm.go('Piotr!')
|
221
|
+
fm.current # => :green
|
222
|
+
```
|
223
|
+
|
224
|
+
### 2.2 single event with multiple from states
|
225
|
+
|
226
|
+
If an event transitions from multiple states to the same state then all the states can be grouped into an array.
|
227
|
+
Altenatively, you can create separte events under the same name for each transition that needs combining.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
fm = FiniteMachine.define do
|
231
|
+
initial :neutral
|
232
|
+
|
233
|
+
events {
|
234
|
+
event :start, :neutral => :one
|
235
|
+
event :shift, :one => :two
|
236
|
+
event :shift, :two => :three
|
237
|
+
event :shift, :three => :four
|
238
|
+
event :slow, [:one, :two, :three] => :one
|
239
|
+
}
|
39
240
|
end
|
241
|
+
```
|
242
|
+
|
243
|
+
## 3 Conditional transitions
|
244
|
+
|
245
|
+
Each event takes an optional `:if` and `:unless` options which act as a predicate for the transition. The `:if` and `:unless` can take a symbol, a string, a Proc or an array. Use `:if` option when you want to specify when the transition **should** happen. If you want to specify when the transition **should not** happen then use `:unless` option.
|
246
|
+
|
247
|
+
### 3.1 Using a Proc
|
248
|
+
|
249
|
+
You can associate the `:if` and `:unless` options with a Proc object that will get called right before transition happens. Proc object gives you ability to write inline condition instead of separate method.
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
fm = FiniteMachine.define do
|
253
|
+
initial :green
|
254
|
+
|
255
|
+
events {
|
256
|
+
event :slow, :green => :yellow, if: -> { return false }
|
257
|
+
}
|
258
|
+
end
|
259
|
+
fm.slow # doesn't transition to :yellow state
|
260
|
+
fm.current # => :green
|
261
|
+
```
|
262
|
+
|
263
|
+
You can also execute methods on an associated object by passing it as an argument to `target` helper.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class Car
|
267
|
+
def turn_engine_on
|
268
|
+
@engine_on = true
|
269
|
+
end
|
270
|
+
|
271
|
+
def turn_engine_off
|
272
|
+
@engine_on = false
|
273
|
+
end
|
274
|
+
|
275
|
+
def engine_on?
|
276
|
+
@engine_on
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
car = Car.new
|
281
|
+
car.trun_engine_on
|
282
|
+
|
283
|
+
fm = FiniteMachine.define do
|
284
|
+
initial :neutral
|
285
|
+
|
286
|
+
target car
|
287
|
+
|
288
|
+
events {
|
289
|
+
event :start, :neutral => :one, if: "engine_on?"
|
290
|
+
}
|
291
|
+
end
|
292
|
+
|
293
|
+
fm.start
|
294
|
+
fm.current # => :one
|
295
|
+
```
|
40
296
|
|
41
|
-
|
297
|
+
### 3.2 Using a Symbol
|
298
|
+
|
299
|
+
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
fsm = FiniteMachine.define do
|
303
|
+
initial :neutral
|
304
|
+
|
305
|
+
target car
|
306
|
+
|
307
|
+
events {
|
308
|
+
event :start, :neutral => :one, if: :engine_on?
|
309
|
+
}
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
### 3.2 Using a String
|
314
|
+
|
315
|
+
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
|
+
|
317
|
+
```ruby
|
318
|
+
fsm = FiniteMachine.define do
|
319
|
+
initial :neutral
|
320
|
+
|
321
|
+
target car
|
322
|
+
|
323
|
+
events {
|
324
|
+
event :start, :neutral => :one, if: "engine_on?"
|
325
|
+
}
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
### 3.4 Combining transition conditions
|
330
|
+
|
331
|
+
When multiple conditions define whether or not a transition should happen, an Array can be used. Moreover, you can apply both `:if` and `:unless` to the same transition.
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
fsm = FiniteMachine.define do
|
335
|
+
initial :green
|
336
|
+
|
337
|
+
events {
|
338
|
+
event :slow, :green => :yellow,
|
339
|
+
if: [ -> { return true }, -> { return true} ],
|
340
|
+
unless: -> { return true }
|
341
|
+
event :stop, :yellow => :red
|
342
|
+
}
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`.
|
347
|
+
|
348
|
+
## 4 Callbacks
|
349
|
+
|
350
|
+
You can consume state machine events and the information they provide by registering a callback. The following main 3 types of callbacks are available in **FiniteMachine**:
|
351
|
+
|
352
|
+
* `on_enter`
|
353
|
+
* `on_transition`
|
354
|
+
* `on_exit`
|
355
|
+
|
356
|
+
Use `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.
|
357
|
+
|
358
|
+
When you subscribe to `:green` state event, the callback will be called whenever someone instruments change for that state. The same will happend upon subscription to event `ready`, namely, the callback will be called each time the state transition method is called.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
fm = FiniteMachine.define do
|
362
|
+
initial :red
|
363
|
+
|
364
|
+
events {
|
365
|
+
event :ready, :red => :yellow
|
366
|
+
event :go, :yellow => :green
|
367
|
+
event :stop, :green => :red
|
368
|
+
}
|
369
|
+
|
370
|
+
callbacks {
|
371
|
+
on_enter :ready { |event, time1, time2, time3| puts "#{time1} #{time2} #{time3} Go!" }
|
372
|
+
on_enter :go { |event, name| puts "Going fast #{name}" }
|
373
|
+
on_enter :stop { |event| ... }
|
374
|
+
}
|
375
|
+
end
|
376
|
+
|
377
|
+
fm.ready(1, 2, 3)
|
42
378
|
fm.go('Piotr!')
|
379
|
+
```
|
380
|
+
|
381
|
+
### 4.1 on_enter
|
382
|
+
|
383
|
+
This method is executed before given event or state change happens. If you provide only a callback without the name for the state or event to listen for, then `any` state and event will be observered.
|
384
|
+
|
385
|
+
### 4.2 on_transition
|
386
|
+
|
387
|
+
This method is executed when given event or state change happens. If you provide only a callback without the name for the state or event to listen for, then `any` state and event will be observered.
|
388
|
+
|
389
|
+
### 4.3 on_exit
|
390
|
+
|
391
|
+
This method is executed after given event or state change happens. If you provide only a callback without the name for the state or event to listen for, then `any` state and event will be observered.
|
392
|
+
|
393
|
+
### 4.4 Parameters
|
394
|
+
|
395
|
+
All callbacks are passed `TransitionEvent` object with the following attributes.
|
396
|
+
|
397
|
+
* name # the event name
|
398
|
+
* from # the state transitioning from
|
399
|
+
* to # the state transitioning to
|
400
|
+
|
401
|
+
followed by the rest of arguments that were passed to the event method.
|
402
|
+
|
403
|
+
### 4.5 Same kind of callbacks
|
404
|
+
|
405
|
+
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
406
|
+
|
407
|
+
```ruby
|
408
|
+
fm = FiniteMachine.define do
|
409
|
+
initial :green
|
410
|
+
|
411
|
+
events {
|
412
|
+
event :slow, :green => :yellow
|
413
|
+
}
|
414
|
+
|
415
|
+
callbacks {
|
416
|
+
on_enter(:yellow) { this_is_run_first }
|
417
|
+
on_enter(:yellow) { then_this }
|
418
|
+
}
|
419
|
+
end
|
420
|
+
fm.slow # => will invoke both callbacks
|
421
|
+
```
|
422
|
+
|
423
|
+
### 4.6 Fluid callbacks
|
424
|
+
|
425
|
+
Callbacks can also be specified as full method calls.
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
fm = FiniteMachine.define do
|
429
|
+
initial :red
|
430
|
+
|
431
|
+
events {
|
432
|
+
event :ready, :red => :yellow
|
433
|
+
event :go, :yellow => :green
|
434
|
+
event :stop, :green => :red
|
435
|
+
}
|
436
|
+
|
437
|
+
callbacks {
|
438
|
+
on_enter_ready { |event| ... }
|
439
|
+
on_enter_go { |event| ... }
|
440
|
+
on_enter_stop { |event| ... }
|
441
|
+
}
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
## 5 Errors
|
446
|
+
|
447
|
+
## 6 Integration
|
448
|
+
|
449
|
+
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(e.i.ActiveRecord) to transform them into state machine or require mixing into exisiting class.
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
|
453
|
+
class Car
|
454
|
+
attr_accessor :reverse_lights
|
455
|
+
|
456
|
+
def turn_reverse_lights_off
|
457
|
+
reverse_lights = false
|
458
|
+
end
|
459
|
+
|
460
|
+
def turn_reverse_lights_on
|
461
|
+
reverse_lights = true
|
462
|
+
end
|
463
|
+
|
464
|
+
def gears
|
465
|
+
@gears ||= FiniteMachine.define do
|
466
|
+
initial :neutral
|
467
|
+
|
468
|
+
target: self
|
469
|
+
|
470
|
+
events {
|
471
|
+
event :start, :neutral => :one
|
472
|
+
event :shift, :one => :two
|
473
|
+
event :shift, :two => :one
|
474
|
+
event :back, [:neutral, :one] => :reverse
|
475
|
+
}
|
476
|
+
|
477
|
+
callbacks {
|
478
|
+
on_enter :reverse do |car, event|
|
479
|
+
car.turnReverseLightsOn
|
480
|
+
end
|
481
|
+
|
482
|
+
on_exit :reverse do |car, event|
|
483
|
+
car.turnReverseLightsOff
|
484
|
+
end
|
485
|
+
|
486
|
+
on_transition do |car, event|
|
487
|
+
puts "shifted from #{event.from} to #{event.to}"
|
488
|
+
end
|
489
|
+
}
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
```
|
495
|
+
|
496
|
+
## 7 Tips
|
497
|
+
|
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.
|
43
499
|
|
44
500
|
## Contributing
|
45
501
|
|