finite_machine 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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]
|
3
3
|
[][travis]
|
4
4
|
[][codeclimate]
|
5
|
+
[][coverage]
|
6
|
+
[][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
|