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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f4ab5eea6230d0ec82cd36c19a351aa37290a4f
4
- data.tar.gz: 01c7525ead98150abc57fb392ada2aa764609381
3
+ metadata.gz: 7582fc3410fbb09056dee6ed167b505ebe6febaf
4
+ data.tar.gz: 3d6336949ace9e8ce3ef3c469346ae6ea1c69044
5
5
  SHA512:
6
- metadata.gz: 13abf559137cbfcdec7b1dc26efbdbc6b22d026012b10fb8277ff1929179b03a1637ccdb398999ef9f5f006210177aeaf6ce3ea88ae394257d82107957399a54
7
- data.tar.gz: 5f22744120ac8aec2341236b6126af612339bca58ddde469a5944090c7156f55ffb35c5a644c5b7bad09c7c3fab1333ba3bdefa731bf058a6bd88b8c59ecda79
6
+ metadata.gz: a1e6145d24909b4762a5be15a85f85c8eac2a07239d3f350ac1eba4283bfe9d4dd517fada486f4b749b97234d32c5b9957b82ccf8bc1ba57441f67a9ba6957d4
7
+ data.tar.gz: 0c948fa0384294b6ead78ae12b6e367d033bd6adf69be4c32420e4ac67cff4a07f565f1870c9d8f62653ccdce51c606cc9f828572e5dcf15ce5d471c80ec6438
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
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 'red'
39
+ initial :red
26
40
 
27
41
  events {
28
- transition name: 'stop', from: 'green', to: 'red'
29
- transition name: 'ready', from: 'red', to: 'yellow'
30
- transition name: 'go', from: 'yellow', to: 'green'
42
+ event :ready, :red => :yellow
43
+ event :go, :yellow => :green
44
+ event :stop, :green => :red
31
45
  }
32
46
 
33
- listeners {
34
- on_stop { |event, from, to| ... }
35
- on_ready { |event, from, to| ... }
36
- on_go { |event, from, to, msg| ... }
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
- fm.read()
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