finite_machine 0.2.0 → 0.3.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/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +12 -1
- data/README.md +130 -73
- data/Rakefile +3 -36
- data/finite_machine.gemspec +1 -3
- data/lib/finite_machine.rb +4 -1
- data/lib/finite_machine/async_call.rb +47 -0
- data/lib/finite_machine/async_proxy.rb +28 -0
- data/lib/finite_machine/callable.rb +5 -5
- data/lib/finite_machine/catchable.rb +2 -2
- data/lib/finite_machine/dsl.rb +16 -9
- data/lib/finite_machine/event.rb +1 -1
- data/lib/finite_machine/event_queue.rb +123 -0
- data/lib/finite_machine/hooks.rb +33 -0
- data/lib/finite_machine/observer.rb +53 -11
- data/lib/finite_machine/state_machine.rb +70 -2
- data/lib/finite_machine/subscribers.rb +5 -2
- data/lib/finite_machine/thread_context.rb +16 -0
- data/lib/finite_machine/threadable.rb +36 -3
- data/lib/finite_machine/transition.rb +47 -4
- data/lib/finite_machine/version.rb +1 -1
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/async_events_spec.rb +68 -0
- data/spec/unit/callable/call_spec.rb +25 -3
- data/spec/unit/callbacks_spec.rb +110 -4
- data/spec/unit/events_spec.rb +5 -0
- data/spec/unit/handlers_spec.rb +53 -0
- data/spec/unit/if_unless_spec.rb +1 -1
- data/spec/unit/is_spec.rb +22 -0
- data/spec/unit/target_spec.rb +17 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- metadata +16 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29fd461f4bd19a780bac89bc363026b214db867f
|
4
|
+
data.tar.gz: 70e229f5f28433618f2408d35136aad7a0a1bb8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00f1f997a94d5f5cb8571921d088a5bc4245001a45463f3b0fc57538094623d94e97c52df76780e957f16279c1bd43d8d0c004595a55a60005f70993fe4873d2
|
7
|
+
data.tar.gz: c62a75b03d74891a0f83bc91ef3bac4e22090f29b0adb9edc398f0290f39dfbcde36513d994373698be17554aa4618916bcd15018275d2afc197f93b0e6944b7
|
data/.rspec
CHANGED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
finite_machine
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
0.3.0 (March 30, 2014)
|
2
|
+
|
3
|
+
* Move development dependencies to Gemfile
|
4
|
+
* Increase test coverage to 95%
|
5
|
+
* Fix bug with event methods dynamic redefinition
|
6
|
+
* Change attr_threadsafe to accept default values
|
7
|
+
* Fix observer respond_to
|
8
|
+
* Add ability to specify callbacks on machine instance
|
9
|
+
* Add once_on type of callback
|
10
|
+
* Add off method for removing callbacks
|
11
|
+
* Add async method to state_machine for asynchronous events firing
|
12
|
+
* Fix Callable to correctly forward arguments
|
13
|
+
* Add state helpers fsm.green? to allow easily check current state
|
14
|
+
|
1
15
|
0.2.0 (March 01, 2014)
|
2
16
|
|
3
17
|
* Ensure correct transition object state
|
data/Gemfile
CHANGED
@@ -1,4 +1,15 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in finite_machine.gemspec
|
4
3
|
gemspec
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem 'rake', '~> 10.1.0'
|
7
|
+
gem 'rspec', '~> 2.14.1'
|
8
|
+
gem 'yard', '~> 0.8.7'
|
9
|
+
end
|
10
|
+
|
11
|
+
group :metrics do
|
12
|
+
gem 'coveralls', '~> 0.7.0'
|
13
|
+
gem 'simplecov', '~> 0.8.2'
|
14
|
+
gem 'yardstick', '~> 0.9.9'
|
15
|
+
end
|
data/README.md
CHANGED
@@ -2,12 +2,16 @@
|
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/finite_machine.png)][gem]
|
3
3
|
[![Build Status](https://secure.travis-ci.org/peter-murach/finite_machine.png?branch=master)][travis]
|
4
4
|
[![Code Climate](https://codeclimate.com/github/peter-murach/finite_machine.png)][codeclimate]
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/peter-murach/finite_machine/badge.png)][coverage]
|
6
|
+
[![Inline docs](http://inch-pages.github.io/github/peter-murach/finite_machine.png)][inchpages]
|
5
7
|
|
6
8
|
[gem]: http://badge.fury.io/rb/finite_machine
|
7
9
|
[travis]: http://travis-ci.org/peter-murach/finite_machine
|
8
10
|
[codeclimate]: https://codeclimate.com/github/peter-murach/finite_machine
|
11
|
+
[coverage]: https://coveralls.io/r/peter-murach/finite_machine
|
12
|
+
[inchpages]: http://inch-pages.github.io/github/peter-murach/finite_machine
|
9
13
|
|
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.
|
14
|
+
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. The machine is event driven with a focus on passing synchronous and asynchronous messages to trigger state transitions.
|
11
15
|
|
12
16
|
## Features
|
13
17
|
|
@@ -18,6 +22,7 @@ A minimal finite state machine with a straightforward and intuitive syntax. You
|
|
18
22
|
* ability to check reachable states
|
19
23
|
* ability to check for terminal state
|
20
24
|
* conditional transitions
|
25
|
+
* sync and async transitions
|
21
26
|
* sync and async callbacks (TODO - only sync)
|
22
27
|
* nested/composable states (TODO)
|
23
28
|
|
@@ -176,6 +181,13 @@ fm.is?(:red) # => true
|
|
176
181
|
fm.is?(:yellow) # => false
|
177
182
|
```
|
178
183
|
|
184
|
+
Moreover, you can use helper methods to check for current state using the state name itself like so
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
fm.red? # => true
|
188
|
+
fm.yellow? # => false
|
189
|
+
```
|
190
|
+
|
179
191
|
### 1.5 can? and cannot?
|
180
192
|
|
181
193
|
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?`.
|
@@ -246,9 +258,9 @@ method on the **FiniteMachine** instance. As a second parameter `event` accepts
|
|
246
258
|
in the form of `:from` and `:to` hash keys or by using the state names themselves as key value pairs.
|
247
259
|
|
248
260
|
```ruby
|
249
|
-
|
250
|
-
|
251
|
-
|
261
|
+
event :start, from: :neutral, to: :first
|
262
|
+
or
|
263
|
+
event :start, :neutral => :first
|
252
264
|
```
|
253
265
|
|
254
266
|
Once specified, the **FiniteMachine** will create custom methods for transitioning between each state.
|
@@ -263,18 +275,35 @@ The following methods trigger transitions for the example state machine.
|
|
263
275
|
In order to transition to the next reachable state, simply call the event's name on the **FiniteMachine** instance.
|
264
276
|
|
265
277
|
```ruby
|
266
|
-
|
267
|
-
|
278
|
+
fm.ready
|
279
|
+
fm.current # => :yellow
|
268
280
|
```
|
269
281
|
|
270
282
|
Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback.
|
271
283
|
|
272
284
|
```ruby
|
273
|
-
|
274
|
-
|
285
|
+
fm.go('Piotr!')
|
286
|
+
fm.current # => :green
|
275
287
|
```
|
276
288
|
|
277
|
-
### 2.2
|
289
|
+
### 2.2 Asynchronous transitions
|
290
|
+
|
291
|
+
By default the transitions will be fired synchronosuly.
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
fm.ready
|
295
|
+
or
|
296
|
+
fm.sync.ready
|
297
|
+
fm.current # => :yellow
|
298
|
+
```
|
299
|
+
|
300
|
+
In order to fire the event transition asynchronously use the `async` scope like so
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
fm.async.ready # => executes in separate Thread
|
304
|
+
```
|
305
|
+
|
306
|
+
### 2.3 single event with multiple from states
|
278
307
|
|
279
308
|
If an event transitions from multiple states to the same state then all the states can be grouped into an array.
|
280
309
|
Altenatively, you can create separte events under the same name for each transition that needs combining.
|
@@ -302,49 +331,49 @@ Each event takes an optional `:if` and `:unless` options which act as a predicat
|
|
302
331
|
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.
|
303
332
|
|
304
333
|
```ruby
|
305
|
-
|
306
|
-
|
334
|
+
fm = FiniteMachine.define do
|
335
|
+
initial :green
|
307
336
|
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
337
|
+
events {
|
338
|
+
event :slow, :green => :yellow, if: -> { return false }
|
339
|
+
}
|
340
|
+
end
|
341
|
+
fm.slow # doesn't transition to :yellow state
|
342
|
+
fm.current # => :green
|
314
343
|
```
|
315
344
|
|
316
345
|
You can also execute methods on an associated object by passing it as an argument to `target` helper.
|
317
346
|
|
318
347
|
```ruby
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
348
|
+
class Car
|
349
|
+
def turn_engine_on
|
350
|
+
@engine_on = true
|
351
|
+
end
|
323
352
|
|
324
|
-
|
325
|
-
|
326
|
-
|
353
|
+
def turn_engine_off
|
354
|
+
@engine_on = false
|
355
|
+
end
|
327
356
|
|
328
|
-
|
329
|
-
|
330
|
-
end
|
357
|
+
def engine_on?
|
358
|
+
@engine_on
|
331
359
|
end
|
360
|
+
end
|
332
361
|
|
333
|
-
|
334
|
-
|
362
|
+
car = Car.new
|
363
|
+
car.turn_engine_on
|
335
364
|
|
336
|
-
|
337
|
-
|
365
|
+
fm = FiniteMachine.define do
|
366
|
+
initial :neutral
|
338
367
|
|
339
|
-
|
368
|
+
target car
|
340
369
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
370
|
+
events {
|
371
|
+
event :start, :neutral => :one, if: "engine_on?"
|
372
|
+
}
|
373
|
+
end
|
345
374
|
|
346
|
-
|
347
|
-
|
375
|
+
fm.start
|
376
|
+
fm.current # => :one
|
348
377
|
```
|
349
378
|
|
350
379
|
### 3.2 Using a Symbol
|
@@ -352,15 +381,15 @@ You can also execute methods on an associated object by passing it as an argumen
|
|
352
381
|
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
|
353
382
|
|
354
383
|
```ruby
|
355
|
-
|
356
|
-
|
384
|
+
fsm = FiniteMachine.define do
|
385
|
+
initial :neutral
|
357
386
|
|
358
|
-
|
387
|
+
target car
|
359
388
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
389
|
+
events {
|
390
|
+
event :start, :neutral => :one, if: :engine_on?
|
391
|
+
}
|
392
|
+
end
|
364
393
|
```
|
365
394
|
|
366
395
|
### 3.3 Using a String
|
@@ -368,15 +397,15 @@ You can also use a symbol corresponding to the name of a method that will get ca
|
|
368
397
|
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.
|
369
398
|
|
370
399
|
```ruby
|
371
|
-
|
372
|
-
|
400
|
+
fsm = FiniteMachine.define do
|
401
|
+
initial :neutral
|
373
402
|
|
374
|
-
|
403
|
+
target car
|
375
404
|
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
405
|
+
events {
|
406
|
+
event :start, :neutral => :one, if: "engine_on?"
|
407
|
+
}
|
408
|
+
end
|
380
409
|
```
|
381
410
|
|
382
411
|
### 3.4 Combining transition conditions
|
@@ -384,16 +413,16 @@ Finally, it's possible to use string that will be evaluated using `eval` and nee
|
|
384
413
|
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.
|
385
414
|
|
386
415
|
```ruby
|
387
|
-
|
388
|
-
|
416
|
+
fsm = FiniteMachine.define do
|
417
|
+
initial :green
|
389
418
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
419
|
+
events {
|
420
|
+
event :slow, :green => :yellow,
|
421
|
+
if: [ -> { return true }, -> { return true} ],
|
422
|
+
unless: -> { return true }
|
423
|
+
event :stop, :yellow => :red
|
424
|
+
}
|
425
|
+
end
|
397
426
|
```
|
398
427
|
|
399
428
|
The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`.
|
@@ -458,13 +487,21 @@ This method is executed after a given event or state change happens. If you prov
|
|
458
487
|
|
459
488
|
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.
|
460
489
|
|
461
|
-
### 4.4
|
490
|
+
### 4.4 once_on
|
491
|
+
|
492
|
+
**FiniteMachine** allows you to listen on initial state change or when the event is fired first time by using the following 3 types of callbacks:
|
493
|
+
|
494
|
+
* `once_on_enter`
|
495
|
+
* `once_on_transition`
|
496
|
+
* `once_on_exit`
|
497
|
+
|
498
|
+
### 4.5 Parameters
|
462
499
|
|
463
500
|
All callbacks get the `TransitionEvent` object with the following attributes.
|
464
501
|
|
465
|
-
* name # the event name
|
466
|
-
* from # the state transitioning from
|
467
|
-
* to # the state transitioning to
|
502
|
+
* `name # the event name`
|
503
|
+
* `from # the state transitioning from`
|
504
|
+
* `to # the state transitioning to`
|
468
505
|
|
469
506
|
followed by the rest of arguments that were passed to the event method.
|
470
507
|
|
@@ -486,7 +523,7 @@ end
|
|
486
523
|
fm.ready(3) # => 'lights switching from red to yellow in 3 seconds'
|
487
524
|
```
|
488
525
|
|
489
|
-
### 4.
|
526
|
+
### 4.6 Same kind of callbacks
|
490
527
|
|
491
528
|
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
492
529
|
|
@@ -506,7 +543,7 @@ end
|
|
506
543
|
fm.slow # => will invoke both callbacks
|
507
544
|
```
|
508
545
|
|
509
|
-
### 4.
|
546
|
+
### 4.7 Fluid callbacks
|
510
547
|
|
511
548
|
Callbacks can also be specified as full method calls.
|
512
549
|
|
@@ -528,7 +565,7 @@ fm = FiniteMachine.define do
|
|
528
565
|
end
|
529
566
|
```
|
530
567
|
|
531
|
-
### 4.
|
568
|
+
### 4.8 Executing methods inside callbacks
|
532
569
|
|
533
570
|
In order to execute method from another object use `target` helper.
|
534
571
|
|
@@ -586,13 +623,33 @@ fm.back # => Go Piotr!
|
|
586
623
|
|
587
624
|
For more complex example see [Integration](#6-integration) section.
|
588
625
|
|
626
|
+
### 4.9 Defining callbacks
|
627
|
+
|
628
|
+
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.
|
629
|
+
|
630
|
+
```ruby
|
631
|
+
fm = FiniteMachine.define do
|
632
|
+
initial :red
|
633
|
+
|
634
|
+
events {
|
635
|
+
event :ready, :red => :yellow
|
636
|
+
event :go, :yellow => :green
|
637
|
+
event :stop, :green => :red
|
638
|
+
}
|
639
|
+
end
|
640
|
+
|
641
|
+
fm.on_enter_yellow do |event|
|
642
|
+
...
|
643
|
+
end
|
644
|
+
```
|
645
|
+
|
589
646
|
## 5 Errors
|
590
647
|
|
591
648
|
By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
|
592
649
|
|
593
|
-
* FiniteMachine::TransitionError
|
594
|
-
* FiniteMachine::InvalidStateError
|
595
|
-
* FiniteMachine::InvalidCallbackError
|
650
|
+
* `FiniteMachine::TransitionError`
|
651
|
+
* `FiniteMachine::InvalidStateError`
|
652
|
+
* `FiniteMachine::InvalidCallbackError`
|
596
653
|
|
597
654
|
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.
|
598
655
|
|
@@ -664,7 +721,7 @@ class Car
|
|
664
721
|
@gears ||= FiniteMachine.define do
|
665
722
|
initial :neutral
|
666
723
|
|
667
|
-
target
|
724
|
+
target context
|
668
725
|
|
669
726
|
events {
|
670
727
|
event :start, :neutral => :one
|
@@ -705,7 +762,7 @@ class Account < ActiveRecord::Base
|
|
705
762
|
|
706
763
|
def manage
|
707
764
|
context = self
|
708
|
-
@
|
765
|
+
@manage ||= FiniteMachine.define do
|
709
766
|
target context
|
710
767
|
|
711
768
|
initial context.state
|
data/Rakefile
CHANGED
@@ -1,41 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
begin
|
4
|
-
require 'rspec/core/rake_task'
|
5
|
-
|
6
|
-
desc 'Run all specs'
|
7
|
-
RSpec::Core::RakeTask.new(:spec) do |task|
|
8
|
-
task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
|
9
|
-
end
|
1
|
+
# encoding: utf-8
|
10
2
|
|
11
|
-
|
12
|
-
desc 'Run unit specs'
|
13
|
-
RSpec::Core::RakeTask.new(:unit) do |task|
|
14
|
-
task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
|
15
|
-
end
|
16
|
-
|
17
|
-
desc 'Run integration specs'
|
18
|
-
RSpec::Core::RakeTask.new(:integration) do |task|
|
19
|
-
task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
|
20
|
-
end
|
21
|
-
end
|
3
|
+
require "bundler/gem_tasks"
|
22
4
|
|
23
|
-
|
24
|
-
%w[spec spec:unit spec:integration].each do |name|
|
25
|
-
task name do
|
26
|
-
$stderr.puts "In order to run #{name}, do `gem install rspec`"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
5
|
+
FileList['tasks/**/*.rake'].each(&method(:import))
|
30
6
|
|
31
7
|
desc 'Run all specs'
|
32
8
|
task ci: %w[ spec ]
|
33
|
-
|
34
|
-
desc 'Load gem inside irb console'
|
35
|
-
task :console do
|
36
|
-
require 'irb'
|
37
|
-
require 'irb/completion'
|
38
|
-
require File.join(__FILE__, '../lib/finite_machine')
|
39
|
-
ARGV.clear
|
40
|
-
IRB.start
|
41
|
-
end
|