finite_machine 0.11.2 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +1 -1
- data/README.md +679 -624
- data/lib/finite_machine.rb +35 -45
- data/lib/finite_machine/async_call.rb +5 -21
- data/lib/finite_machine/callable.rb +4 -4
- data/lib/finite_machine/catchable.rb +24 -14
- data/lib/finite_machine/choice_merger.rb +20 -20
- data/lib/finite_machine/const.rb +16 -0
- data/lib/finite_machine/definition.rb +3 -3
- data/lib/finite_machine/dsl.rb +98 -151
- data/lib/finite_machine/env.rb +4 -2
- data/lib/finite_machine/event_definition.rb +7 -15
- data/lib/finite_machine/{events_chain.rb → events_map.rb} +40 -53
- data/lib/finite_machine/hook_event.rb +60 -61
- data/lib/finite_machine/hooks.rb +44 -36
- data/lib/finite_machine/listener.rb +2 -2
- data/lib/finite_machine/logger.rb +5 -4
- data/lib/finite_machine/{event_queue.rb → message_queue.rb} +76 -32
- data/lib/finite_machine/observer.rb +71 -34
- data/lib/finite_machine/safety.rb +16 -14
- data/lib/finite_machine/state_definition.rb +3 -5
- data/lib/finite_machine/state_machine.rb +93 -76
- data/lib/finite_machine/state_parser.rb +55 -83
- data/lib/finite_machine/subscribers.rb +2 -2
- data/lib/finite_machine/threadable.rb +3 -1
- data/lib/finite_machine/transition.rb +34 -34
- data/lib/finite_machine/transition_builder.rb +23 -32
- data/lib/finite_machine/transition_event.rb +12 -11
- data/lib/finite_machine/two_phase_lock.rb +8 -6
- data/lib/finite_machine/undefined_transition.rb +5 -6
- data/lib/finite_machine/version.rb +2 -2
- metadata +58 -142
- data/.gitignore +0 -18
- data/.rspec +0 -5
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -26
- data/Gemfile +0 -15
- data/Rakefile +0 -8
- data/assets/finite_machine_logo.png +0 -0
- data/examples/atm.rb +0 -45
- data/examples/bug_system.rb +0 -145
- data/finite_machine.gemspec +0 -23
- data/lib/finite_machine/async_proxy.rb +0 -30
- data/spec/integration/system_spec.rb +0 -95
- data/spec/spec_helper.rb +0 -33
- data/spec/unit/alias_target_spec.rb +0 -108
- data/spec/unit/async_events_spec.rb +0 -138
- data/spec/unit/callable/call_spec.rb +0 -113
- data/spec/unit/callbacks_spec.rb +0 -942
- data/spec/unit/can_spec.rb +0 -98
- data/spec/unit/choice_spec.rb +0 -331
- data/spec/unit/define_spec.rb +0 -55
- data/spec/unit/definition_spec.rb +0 -115
- data/spec/unit/event_names_spec.rb +0 -19
- data/spec/unit/event_queue_spec.rb +0 -52
- data/spec/unit/events_chain/add_spec.rb +0 -25
- data/spec/unit/events_chain/cancel_transitions_spec.rb +0 -22
- data/spec/unit/events_chain/choice_transition_spec.rb +0 -28
- data/spec/unit/events_chain/clear_spec.rb +0 -15
- data/spec/unit/events_chain/events_spec.rb +0 -18
- data/spec/unit/events_chain/inspect_spec.rb +0 -24
- data/spec/unit/events_chain/match_transition_spec.rb +0 -37
- data/spec/unit/events_chain/move_to_spec.rb +0 -48
- data/spec/unit/events_chain/states_for_spec.rb +0 -17
- data/spec/unit/events_spec.rb +0 -459
- data/spec/unit/handlers_spec.rb +0 -152
- data/spec/unit/hook_event/build_spec.rb +0 -15
- data/spec/unit/hook_event/eql_spec.rb +0 -36
- data/spec/unit/hook_event/infer_default_name_spec.rb +0 -13
- data/spec/unit/hook_event/initialize_spec.rb +0 -25
- data/spec/unit/hook_event/notify_spec.rb +0 -14
- data/spec/unit/hooks/call_spec.rb +0 -24
- data/spec/unit/hooks/clear_spec.rb +0 -16
- data/spec/unit/hooks/inspect_spec.rb +0 -17
- data/spec/unit/hooks/register_spec.rb +0 -22
- data/spec/unit/if_unless_spec.rb +0 -353
- data/spec/unit/initial_spec.rb +0 -222
- data/spec/unit/inspect_spec.rb +0 -17
- data/spec/unit/is_spec.rb +0 -55
- data/spec/unit/log_transitions_spec.rb +0 -30
- data/spec/unit/logger_spec.rb +0 -38
- data/spec/unit/respond_to_spec.rb +0 -38
- data/spec/unit/state_parser/inspect_spec.rb +0 -25
- data/spec/unit/state_parser/parse_spec.rb +0 -59
- data/spec/unit/states_spec.rb +0 -34
- data/spec/unit/subscribers_spec.rb +0 -42
- data/spec/unit/target_spec.rb +0 -225
- data/spec/unit/terminated_spec.rb +0 -95
- data/spec/unit/transition/check_conditions_spec.rb +0 -54
- data/spec/unit/transition/inspect_spec.rb +0 -25
- data/spec/unit/transition/matches_spec.rb +0 -23
- data/spec/unit/transition/states_spec.rb +0 -31
- data/spec/unit/transition/to_state_spec.rb +0 -27
- data/spec/unit/trigger_spec.rb +0 -22
- data/spec/unit/undefined_transition/eql_spec.rb +0 -17
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2d4822c18a4beea1845f39a33e910051412c7fea3479783b361897ecbc60b006
|
4
|
+
data.tar.gz: ec4f8663f7ede7eb0357b9404b814ca670a523923f6c0b4435c2f09acd6c2e32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57bdadc9ea5349045fe6711983dae949e8a8e78d569dbe7e91ac410b7afec7f1a2cfbb95f6b400b068ffcb1a7c4f783e84575f0680c2956ec5dc8473d91ee312
|
7
|
+
data.tar.gz: b71c1fe3d0f8701fff5060b8dd6882fa9682e2a79dddbfda865d6d553c9130b896690ad16b4607a567e4d03f95a5dd0b4d33517cf008883e7dd1eab1851e411e
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,74 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v0.14.0] - 2020-09-12
|
4
|
+
|
5
|
+
### Added
|
6
|
+
* Add ability to declare alias target inside definition
|
7
|
+
* Add #respond_to_missing? to DSL to check if a given method is supported
|
8
|
+
* Add current state attribute to StateMachine#inspect
|
9
|
+
|
10
|
+
## [v0.13.0] - 2020-05-13
|
11
|
+
|
12
|
+
### Added
|
13
|
+
* Add sync as a dependency
|
14
|
+
* Add metadata to gemspec
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
* Change StateMachine#final_state to #terminal_states by Brad Gessler(@bradgessler)
|
18
|
+
* Change to remove artefacts like tests & tasks from gemspec
|
19
|
+
|
20
|
+
### Fixed
|
21
|
+
* Fix Ruby 2.7 keyword arguments warnings
|
22
|
+
* Fix MessageQueue deadlock by Pavel Rosický(@ahorek)
|
23
|
+
|
24
|
+
## [v0.12.1] - 2019-07-12
|
25
|
+
|
26
|
+
### Changed
|
27
|
+
* Change to relax dev dependencies versions
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
* Fix FiniteMachine.new to stop coercing object target that responds to to_hash into options
|
31
|
+
|
32
|
+
## [v0.12.0] - 2018-11-11
|
33
|
+
|
34
|
+
### Added
|
35
|
+
* Add concurrent-ruby as dependency
|
36
|
+
* Add FiniteMachine#new for declaring state machines
|
37
|
+
* Add Observer#cancel_event for cancelling event transitions in callbacks, instead of using callback return value
|
38
|
+
* Add Const for declaring unique machine constants
|
39
|
+
* Add :auto_methods configuration option for disabling automatic conversion of event names into methods
|
40
|
+
|
41
|
+
### Changed
|
42
|
+
* Change gemspec to require Ruby >= 2.0
|
43
|
+
* Change FiniteMachine#define to create machine class instances
|
44
|
+
* Change EventsChain to EventsMap and use Concurrent::Map for holding event transitions
|
45
|
+
* Change Hooks to use Concurrent::Map for storing callbacks
|
46
|
+
* Change MessageQueue to use mutex to synchronize access
|
47
|
+
* Change StateParser to remove internal state and use class methods instead
|
48
|
+
* Change Observer to create callbacks queue on demand
|
49
|
+
* Change :any key to be a unique constant ANY_EVENT and ANY_STATE
|
50
|
+
* Change #event_names to #events for retrieving all events
|
51
|
+
* Remove thread synchronization from AsyncCall, TransitinEvent, HookEvent, DSL, Hooks, TransitionBuilder, ChoiceMerger objects
|
52
|
+
* Remove #async call from StateMachine
|
53
|
+
* Remove #target, #alias_target, #callbacks, #events and #handlers calls from DSL
|
54
|
+
|
55
|
+
### Fixed
|
56
|
+
* Fix StateParser to raise error without nil
|
57
|
+
* Fix to rollback to current state when an error occurs
|
58
|
+
|
59
|
+
## [v0.11.3] - 2016-03-04
|
60
|
+
|
61
|
+
### Added
|
62
|
+
* Add performance tests & memory usage benchmarks
|
63
|
+
|
64
|
+
### Changed
|
65
|
+
* Change EventQueue to MessageQueue for handling generic asynchronous messages
|
66
|
+
* Split async behaviour to use CallbackQueue for observed callbacks and EventQueue for async event triggers.
|
67
|
+
* Change AsyncProxy and Observer to lazy load message queue
|
68
|
+
|
69
|
+
### Fixed
|
70
|
+
* Fix memory leak - issue #42 with help from @craiglittle
|
71
|
+
|
3
72
|
## [v0.11.2] - 2015-12-30
|
4
73
|
|
5
74
|
### Added
|
@@ -237,6 +306,17 @@
|
|
237
306
|
### Fixed
|
238
307
|
* Fix bug - callback event object returns correct from state
|
239
308
|
|
309
|
+
## [v0.1.0] - 2014-02-09
|
310
|
+
|
311
|
+
## [v0.0.1] - 2014-01-10
|
312
|
+
|
313
|
+
* Initial release
|
314
|
+
|
315
|
+
[v0.14.0]: https://github.com/peter-murach/finite_machine/compare/v0.13.0...v0.14.0
|
316
|
+
[v0.13.0]: https://github.com/peter-murach/finite_machine/compare/v0.12.1...v0.13.0
|
317
|
+
[v0.12.1]: https://github.com/peter-murach/finite_machine/compare/v0.12.0...v0.12.1
|
318
|
+
[v0.12.0]: https://github.com/peter-murach/finite_machine/compare/v0.11.3...v0.12.0
|
319
|
+
[v0.11.3]: https://github.com/peter-murach/finite_machine/compare/v0.11.2...v0.11.3
|
240
320
|
[v0.11.2]: https://github.com/peter-murach/finite_machine/compare/v0.11.1...v0.11.2
|
241
321
|
[v0.11.1]: https://github.com/peter-murach/finite_machine/compare/v0.11.0...v0.11.1
|
242
322
|
[v0.11.0]: https://github.com/peter-murach/finite_machine/compare/v0.10.2...v0.11.0
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,41 +1,44 @@
|
|
1
1
|
<div align="center">
|
2
|
-
<a href="
|
2
|
+
<a href="https://piotrmurach.github.io/finite_machine/"><img width="236" src="https://github.com/piotrmurach/finite_machine/raw/master/assets/finite_machine_logo.png" alt="finite machine logo" /></a>
|
3
3
|
</div>
|
4
|
+
|
4
5
|
# FiniteMachine
|
6
|
+
|
5
7
|
[![Gem Version](https://badge.fury.io/rb/finite_machine.svg)][gem]
|
6
|
-
[![Build Status](https://secure.travis-ci.org/
|
7
|
-
[![
|
8
|
-
[![
|
9
|
-
[![
|
10
|
-
[![
|
8
|
+
[![Build Status](https://secure.travis-ci.org/piotrmurach/finite_machine.svg?branch=master)][travis]
|
9
|
+
[![Build status](https://ci.appveyor.com/api/projects/status/8ho4ijacpr7b4f4t?svg=true)][appveyor]
|
10
|
+
[![Code Climate](https://codeclimate.com/github/piotrmurach/finite_machine/badges/gpa.svg)][codeclimate]
|
11
|
+
[![Coverage Status](https://coveralls.io/repos/github/piotrmurach/finite_machine/badge.svg?branch=master)][coverage]
|
12
|
+
[![Inline docs](http://inch-ci.org/github/piotrmurach/finite_machine.svg)][inchpages]
|
13
|
+
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
|
11
14
|
|
12
15
|
[gem]: http://badge.fury.io/rb/finite_machine
|
13
|
-
[travis]: http://travis-ci.org/
|
14
|
-
[
|
15
|
-
[
|
16
|
-
[
|
17
|
-
[
|
16
|
+
[travis]: http://travis-ci.org/piotrmurach/finite_machine
|
17
|
+
[appveyor]: https://ci.appveyor.com/project/piotrmurach/finite-machine
|
18
|
+
[codeclimate]: https://codeclimate.com/github/piotrmurach/finite_machine
|
19
|
+
[coverage]: https://coveralls.io/github/piotrmurach/finite_machine?branch=master
|
20
|
+
[inchpages]: http://inch-ci.org/github/piotrmurach/finite_machine
|
21
|
+
[gitter]: https://gitter.im/piotrmurach/finite_machine
|
18
22
|
|
19
|
-
> A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and
|
23
|
+
> A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and transitions and register callbacks to watch for triggered transitions.
|
20
24
|
|
21
25
|
## Features
|
22
26
|
|
23
27
|
* plain object state machine
|
24
|
-
* easy custom object integration
|
25
|
-
* natural DSL for declaring events, callbacks and
|
26
|
-
*
|
27
|
-
* ability to check reachable
|
28
|
-
* ability to check for terminal state
|
29
|
-
* transition guard conditions
|
30
|
-
* dynamic
|
31
|
-
*
|
32
|
-
* sync and async callbacks
|
28
|
+
* easy [custom object integration](#29-target)
|
29
|
+
* natural DSL for declaring events, callbacks and exception handlers
|
30
|
+
* [callbacks](#4-callbacks) for state and event changes
|
31
|
+
* ability to check [reachable](#28-can-and-cannot) state(s)
|
32
|
+
* ability to check for [terminal](#25-terminal) state(s)
|
33
|
+
* transition [guard conditions](#38-conditional-transitions)
|
34
|
+
* dynamic [choice pseudostates](#39-choice-pseudostates)
|
35
|
+
* thread safe
|
33
36
|
|
34
37
|
## Installation
|
35
38
|
|
36
39
|
Add this line to your application's Gemfile:
|
37
40
|
|
38
|
-
gem
|
41
|
+
gem "finite_machine"
|
39
42
|
|
40
43
|
Then execute:
|
41
44
|
|
@@ -48,143 +51,225 @@ Or install it yourself as:
|
|
48
51
|
## Contents
|
49
52
|
|
50
53
|
* [1. Usage](#1-usage)
|
51
|
-
|
52
|
-
* [1
|
53
|
-
* [
|
54
|
-
* [
|
55
|
-
* [
|
56
|
-
* [
|
57
|
-
* [
|
58
|
-
* [
|
59
|
-
|
60
|
-
* [
|
61
|
-
* [2.
|
62
|
-
|
63
|
-
* [2.
|
64
|
-
* [2.
|
65
|
-
* [2.
|
66
|
-
|
67
|
-
* [
|
68
|
-
* [2
|
69
|
-
* [
|
70
|
-
* [3.
|
71
|
-
* [3.
|
72
|
-
* [3.
|
73
|
-
* [3.
|
74
|
-
* [3.
|
75
|
-
* [
|
76
|
-
|
77
|
-
|
78
|
-
* [
|
79
|
-
* [
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
* [
|
84
|
-
* [
|
85
|
-
* [
|
86
|
-
* [
|
87
|
-
* [5
|
88
|
-
* [
|
89
|
-
* [
|
90
|
-
* [
|
91
|
-
* [
|
92
|
-
* [
|
93
|
-
* [
|
94
|
-
|
95
|
-
* [
|
96
|
-
|
97
|
-
* [
|
98
|
-
* [
|
99
|
-
* [
|
100
|
-
|
101
|
-
* [
|
102
|
-
* [
|
103
|
-
* [
|
104
|
-
|
105
|
-
|
54
|
+
* [2. API](#2-api)
|
55
|
+
* [2.1 new](#21-new)
|
56
|
+
* [2.2 define](#22-define)
|
57
|
+
* [2.3 current](#23-current)
|
58
|
+
* [2.4 initial](#24-initial)
|
59
|
+
* [2.5 terminal](#25-terminal)
|
60
|
+
* [2.6 is?](#26-is)
|
61
|
+
* [2.7 trigger](#27-trigger)
|
62
|
+
* [2.7.1 :auto_methods](#271-auto_methods)
|
63
|
+
* [2.8 can? and cannot?](#28-can-and-cannot)
|
64
|
+
* [2.9 target](#29-target)
|
65
|
+
* [2.9.1 :alias_target](#27-alias_target)
|
66
|
+
* [2.10 restore!](#210-restore)
|
67
|
+
* [2.11 states](#211-states)
|
68
|
+
* [2.12 events](#212-events)
|
69
|
+
* [3. States and Transitions](#3-states-and-transitions)
|
70
|
+
* [3.1 Triggering transitions](#31-triggering-transitions)
|
71
|
+
* [3.2 Dangerous transitions](#32-dangerous-transitions)
|
72
|
+
* [3.3 Multiple from states](#33-multiple-from-states)
|
73
|
+
* [3.4 any_state transitions](#34-any_state-transitions)
|
74
|
+
* [3.5 Collapsing transitions](#35-collapsing-transitions)
|
75
|
+
* [3.6 Silent transitions](#36-silent-transitions)
|
76
|
+
* [3.7 Logging transitions](#37-logging-transitions)
|
77
|
+
* [3.8 Conditional transitions](#38-conditional-transitions)
|
78
|
+
* [3.8.1 Using a Proc](#381-using-a-proc)
|
79
|
+
* [3.8.2 Using a Symbol](#382-using-a-symbol)
|
80
|
+
* [3.8.3 Using a String](#383-using-a-string)
|
81
|
+
* [3.8.4 Combining transition conditions](#384-combining-transition-conditions)
|
82
|
+
* [3.9 Choice pseudostates](#39-choice-pseudostates)
|
83
|
+
* [3.9.1 Dynamic choice conditions](#391-dynamic-choice-conditions)
|
84
|
+
* [3.9.2 Multiple from states](#392-multiple-from-states)
|
85
|
+
* [4. Callbacks](#4-callbacks)
|
86
|
+
* [4.1 on_(enter|transition|exit)](#41-on_entertransitionexit)
|
87
|
+
* [4.2 on_(before|after)](#42-on_beforeafter)
|
88
|
+
* [4.3 once_on](#43-once_on)
|
89
|
+
* [4.4 Execution sequence](#44-execution-sequence)
|
90
|
+
* [4.5 Callback parameters](#45-callback-parameters)
|
91
|
+
* [4.6 Duplicate callbacks](#46-duplicate-callbacks)
|
92
|
+
* [4.7 Fluid callbacks](#47-fluid-callbacks)
|
93
|
+
* [4.8 Methods inside callbacks](#48-methods-inside-callbacks)
|
94
|
+
* [4.9 Cancelling callbacks](#49-cancelling-callbacks)
|
95
|
+
* [4.10 Asynchronous callbacks](#410-asynchronous-callbacks)
|
96
|
+
* [4.11 Instance callbacks](#411-instance-callbacks)
|
97
|
+
* [5. Error Handling](#5-error-handling)
|
98
|
+
* [5.1 Using target](#51-using-target)
|
99
|
+
* [6. Stand-alone](#6-stand-alone)
|
100
|
+
* [6.1 Creating a Definition](#61-creating-a-definition)
|
101
|
+
* [6.2 Targeting definition](#62-targeting-definition)
|
102
|
+
* [6.3 Definition inheritance](#63-definition-inheritance)
|
103
|
+
* [7. Integration](#7-integration)
|
104
|
+
* [7.1 Plain Ruby Objects](#71-plain-ruby-objects)
|
105
|
+
* [7.2 ActiveRecord](#72-activerecord)
|
106
|
+
* [7.3 Transactions](#73-transactions)
|
107
|
+
* [8. Tips](#8-tips)
|
108
|
+
|
109
|
+
## 1. Usage
|
106
110
|
|
107
111
|
Here is a very simple example of a state machine:
|
108
112
|
|
109
113
|
```ruby
|
110
|
-
fm = FiniteMachine.
|
114
|
+
fm = FiniteMachine.new do
|
111
115
|
initial :red
|
112
116
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
}
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
on_after(:go) { |event| ... }
|
122
|
-
on_before(:stop) { |event| ... }
|
123
|
-
}
|
117
|
+
event :ready, :red => :yellow
|
118
|
+
event :go, :yellow => :green
|
119
|
+
event :stop, :green => :red
|
120
|
+
|
121
|
+
on_before(:ready) { |event| ... }
|
122
|
+
on_exit(:yellow) { |event| ... }
|
123
|
+
on_enter(:green) { |event| ... }
|
124
|
+
on_after(:stop) { |event| ... }
|
124
125
|
end
|
125
126
|
```
|
126
127
|
|
127
|
-
|
128
|
+
By calling the `new` method on **FiniteMachine**, you gain access to a powerful DSL for expressing transitions and registering callbacks.
|
129
|
+
|
130
|
+
Having declared the states and transitions, you can check current state:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
fm.current # => :red
|
134
|
+
````
|
135
|
+
|
136
|
+
And then trigger transitions using the `trigger`:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
fm.trigger(:ready)
|
140
|
+
```
|
141
|
+
|
142
|
+
Or you can use direct method calls:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
fm.ready
|
146
|
+
```
|
147
|
+
|
148
|
+
Read [States and Transitions](#3-states-and-transitions) and [Callbacks](#4-callbacks) sections for more details.
|
128
149
|
|
129
|
-
Alternatively, you can construct the machine like a regular object
|
150
|
+
Alternatively, you can construct the state machine like a regular object using the same DSL methods. Similar machine could be reimplemented as follows:
|
130
151
|
|
131
152
|
```ruby
|
132
|
-
fm = FiniteMachine.new
|
153
|
+
fm = FiniteMachine.new(initial: :red)
|
133
154
|
fm.event(:ready, :red => :yellow)
|
134
155
|
fm.event(:go, :yellow => :green)
|
135
156
|
fm.event(:stop, :green => :red)
|
136
157
|
fm.on_before(:ready) { |event| ... }
|
137
|
-
fm.
|
138
|
-
fm.
|
158
|
+
fm.on_exit(:yellow) { |event| ... }
|
159
|
+
fm.on_enter(:green) { |event| ... }
|
160
|
+
fm.on_after(:stop) { |event| ... }
|
161
|
+
```
|
162
|
+
|
163
|
+
## 2. API
|
164
|
+
|
165
|
+
### 2.1 new
|
166
|
+
|
167
|
+
In most cases you will want to create an instance of **FiniteMachine** class using the `new` method. At the bare minimum you need specify the transition events inside a block using the `event` helper:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
fm = FiniteMachine.new do
|
171
|
+
initial :green
|
172
|
+
|
173
|
+
event :slow, :green => :yellow
|
174
|
+
event :stop, :yellow => :red
|
175
|
+
event :ready, :red => :yellow
|
176
|
+
event :go, :yellow => :green
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
Alternatively, you can skip block definition and instead call DSL methods directly on the state machine instance:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
fm = FiniteMachine.new
|
184
|
+
fm.initial(:green)
|
185
|
+
fm.event(:slow, :green => :yellow)
|
186
|
+
fm.event(:stop, :yellow => :red)
|
187
|
+
fm.event(:ready,:red => :yellow)
|
188
|
+
fm.event(:go, :yellow => :green)
|
139
189
|
```
|
140
190
|
|
141
|
-
|
191
|
+
As a guiding rule, any method exposed via DSL is available as a regular method call on the state machine instance.
|
192
|
+
|
193
|
+
### 2.2 define
|
194
|
+
|
195
|
+
To create a reusable definition for a state machine use `define` method. By calling `define` you're creating an anonymous class that can act as a factory for state machines. For example, below we create a `TrafficLights` class that contains our state machine definition:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
TrafficLights = FiniteMachine.define do
|
199
|
+
initial :green
|
200
|
+
|
201
|
+
event :slow, :green => :yellow
|
202
|
+
event :stop, :yellow => :red
|
203
|
+
event :ready, :red => :yellow
|
204
|
+
event :go, :yellow => :green
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
Then we can create however many instance of above class:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
lights_fm_a = TrafficLights.new
|
212
|
+
lights_fm_b = TrafficLights.new
|
213
|
+
```
|
214
|
+
|
215
|
+
Each instance will start in consistent state:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
lights_fm_a.current # => :green
|
219
|
+
lights_fm_b.current # => :green
|
220
|
+
```
|
221
|
+
|
222
|
+
We can then trigger event for one instance and not the other:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
lights_fm_a.slow
|
226
|
+
lights_fm_a.current # => :yellow
|
227
|
+
lights_fm_b.current # => :green
|
228
|
+
```
|
229
|
+
|
230
|
+
### 2.3 current
|
142
231
|
|
143
232
|
The **FiniteMachine** allows you to query the current state by calling the `current` method.
|
144
233
|
|
145
234
|
```ruby
|
146
|
-
|
235
|
+
fm.current # => :red
|
147
236
|
```
|
148
237
|
|
149
|
-
###
|
238
|
+
### 2.4 initial
|
150
239
|
|
151
240
|
There are number of ways to provide the initial state in **FiniteMachine** depending on your requirements.
|
152
241
|
|
153
|
-
By default the **FiniteMachine** will be in the `:none` state and you will need to provide an event to transition out of this state.
|
242
|
+
By default the **FiniteMachine** will be in the `:none` state and you will need to provide an explicit event to transition out of this state.
|
154
243
|
|
155
244
|
```ruby
|
156
|
-
fm = FiniteMachine.
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
event :stop, :yellow => :red
|
161
|
-
}
|
245
|
+
fm = FiniteMachine.new do
|
246
|
+
event :init, :none => :green
|
247
|
+
event :slow, :green => :yellow
|
248
|
+
event :stop, :yellow => :red
|
162
249
|
end
|
163
250
|
|
164
251
|
fm.current # => :none
|
165
|
-
fm.
|
252
|
+
fm.init # => true
|
166
253
|
fm.current # => :green
|
167
254
|
```
|
168
255
|
|
169
|
-
If you specify initial state using the `initial` helper, the state machine will be created already in that state.
|
256
|
+
If you specify initial state using the `initial` helper, then the state machine will be created already in that state and an implicit `init` event will be created for you and automatically triggered upon the state machine initialization.
|
170
257
|
|
171
258
|
```ruby
|
172
|
-
fm = FiniteMachine.
|
173
|
-
initial :green
|
259
|
+
fm = FiniteMachine.new do
|
260
|
+
initial :green # fires init event that transitions from :none to :green state
|
174
261
|
|
175
|
-
|
176
|
-
|
177
|
-
event :stop, :yellow => :red
|
178
|
-
}
|
262
|
+
event :slow, :green => :yellow
|
263
|
+
event :stop, :yellow => :red
|
179
264
|
end
|
180
265
|
|
181
266
|
fm.current # => :green
|
182
267
|
```
|
183
268
|
|
184
|
-
|
269
|
+
Or by passing named argument `:initial` like so:
|
185
270
|
|
186
271
|
```ruby
|
187
|
-
fm = FiniteMachine.
|
272
|
+
fm = FiniteMachine.new(initial: :green) do
|
188
273
|
...
|
189
274
|
end
|
190
275
|
```
|
@@ -192,13 +277,11 @@ end
|
|
192
277
|
If you want to defer setting the initial state, pass the `:defer` option to the `initial` helper. By default **FiniteMachine** will create `init` event that will allow to transition from `:none` state to the new state.
|
193
278
|
|
194
279
|
```ruby
|
195
|
-
fm = FiniteMachine.
|
280
|
+
fm = FiniteMachine.new do
|
196
281
|
initial :green, defer: true # Defer calling :init event
|
197
282
|
|
198
|
-
|
199
|
-
|
200
|
-
event :stop, :yellow => :red
|
201
|
-
}
|
283
|
+
event :slow, :green => :yellow
|
284
|
+
event :stop, :yellow => :red
|
202
285
|
end
|
203
286
|
fm.current # => :none
|
204
287
|
fm.init # execute initial transition
|
@@ -208,13 +291,11 @@ fm.current # => :green
|
|
208
291
|
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.
|
209
292
|
|
210
293
|
```ruby
|
211
|
-
fm = FiniteMachine.
|
294
|
+
fm = FiniteMachine.new do
|
212
295
|
initial :green, event: :start, defer: true # Rename event from :init to :start
|
213
296
|
|
214
|
-
|
215
|
-
|
216
|
-
event :stop, :yellow => :red
|
217
|
-
}
|
297
|
+
event :slow, :green => :yellow
|
298
|
+
event :stop, :yellow => :red
|
218
299
|
end
|
219
300
|
|
220
301
|
fm.current # => :none
|
@@ -222,34 +303,30 @@ fm.start # => call the renamed event
|
|
222
303
|
fm.current # => :green
|
223
304
|
```
|
224
305
|
|
225
|
-
By default the `initial` does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the `silent` option set to `false` like so
|
306
|
+
By default the `initial` does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the `silent` option set to `false` like so:
|
226
307
|
|
227
308
|
```ruby
|
228
|
-
fm = FiniteMachine.
|
309
|
+
fm = FiniteMachine.new do
|
229
310
|
initial :green, silent: false # callbacks are triggered
|
230
311
|
|
231
|
-
|
232
|
-
|
233
|
-
event :stop, :yellow => :red
|
234
|
-
}
|
312
|
+
event :slow, :green => :yellow
|
313
|
+
event :stop, :yellow => :red
|
235
314
|
end
|
236
315
|
```
|
237
316
|
|
238
|
-
###
|
317
|
+
### 2.5 terminal
|
239
318
|
|
240
|
-
To specify a final state **FiniteMachine** uses the `terminal` method.
|
319
|
+
To specify a final state **FiniteMachine** uses the `terminal` method.
|
241
320
|
|
242
321
|
```ruby
|
243
|
-
fm = FiniteMachine.
|
322
|
+
fm = FiniteMachine.new do
|
244
323
|
initial :green
|
245
324
|
|
246
325
|
terminal :red
|
247
326
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
event :go, :red => :green
|
252
|
-
}
|
327
|
+
event :slow, :green => :yellow
|
328
|
+
event :stop, :yellow => :red
|
329
|
+
event :go, :red => :green
|
253
330
|
end
|
254
331
|
```
|
255
332
|
|
@@ -263,7 +340,27 @@ fm.stop # => true
|
|
263
340
|
fm.terminated? # => true
|
264
341
|
```
|
265
342
|
|
266
|
-
|
343
|
+
The `terminal` can accept more than one state.
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
fm = FiniteMachine.new do
|
347
|
+
initial :open
|
348
|
+
|
349
|
+
terminal :close, :canceled
|
350
|
+
|
351
|
+
event :resolve, :open => :close
|
352
|
+
event :decline, :open => :canceled
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
And the terminal state can be checked using `terminated?`:
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
fm.decline
|
360
|
+
fm.terminated? # => true
|
361
|
+
```
|
362
|
+
|
363
|
+
### 2.6 is?
|
267
364
|
|
268
365
|
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.
|
269
366
|
|
@@ -279,9 +376,47 @@ fm.red? # => true
|
|
279
376
|
fm.yellow? # => false
|
280
377
|
```
|
281
378
|
|
282
|
-
###
|
379
|
+
### 2.7 trigger
|
380
|
+
|
381
|
+
Transition events can be fired by calling the `trigger` method with the event name and remaining arguments as data. The return value is either `true` or `false` depending whether the transition succeeded or not:
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
fm.trigger(:ready) # => true
|
385
|
+
fm.trigger(:ready, "one", "two", "three") # => true
|
386
|
+
```
|
387
|
+
|
388
|
+
By default, the **FiniteMachine** automatically converts all the transition event names into methods:
|
389
|
+
|
390
|
+
```ruby
|
391
|
+
fm.ready # => true
|
392
|
+
fm.ready("one", "two", "three") # => true
|
393
|
+
```
|
394
|
+
|
395
|
+
Please see [States and Transitions](#3-states-and-transitions) for in-depth treatment of firing transitions.
|
283
396
|
|
284
|
-
|
397
|
+
|
398
|
+
#### 2.7.1 `:auto_methods`
|
399
|
+
|
400
|
+
By default, all event names will be converted by **FiniteMachine** into method names. This also means that you won't be able to use event names such as `:fail` or `:trigger` as these are already defined on the machine instance. In situations when you wish to use any event name for your event names use `:auto_methods` keyword to disable automatic methods generation. For example, to define `:fail` event:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
fm = FiniteMachine.new(auto_methods: false) do
|
404
|
+
initial :green
|
405
|
+
|
406
|
+
event :fail, :green => :red
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
And then you can use `trigger` to fire the event:
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
fm.trigger(:fail)
|
414
|
+
fm.current # => :red
|
415
|
+
```
|
416
|
+
|
417
|
+
### 2.8 `can?` and `cannot?`
|
418
|
+
|
419
|
+
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`. The `cannot?` is simply the inverse of `can?`.
|
285
420
|
|
286
421
|
```ruby
|
287
422
|
fm.can?(:ready) # => true
|
@@ -293,14 +428,13 @@ fm.cannot?(:go) # => true
|
|
293
428
|
The `can?` and `cannot?` helper methods take into account the `:if` and `:unless` conditions applied to events. The set of values that `:if` or `:unless` condition takes as block parameter can be passed in directly via `can?` and `cannot?` methods' arguments, after the name of the event. For instance,
|
294
429
|
|
295
430
|
```ruby
|
296
|
-
fm = FiniteMachine.
|
431
|
+
fm = FiniteMachine.new do
|
297
432
|
initial :green
|
298
433
|
|
299
|
-
|
300
|
-
|
301
|
-
event :stop, :yellow => :red, if: proc { |_, param| :breaks == param }
|
302
|
-
}
|
434
|
+
event :slow, :green => :yellow
|
435
|
+
event :stop, :yellow => :red, if: ->(_, param) { :breaks == param }
|
303
436
|
end
|
437
|
+
|
304
438
|
fm.can?(:slow) # => true
|
305
439
|
fm.can?(:stop) # => false
|
306
440
|
|
@@ -309,95 +443,106 @@ fm.can?(:stop, :breaks) # => true
|
|
309
443
|
fm.can?(:stop, :no_breaks) # => false
|
310
444
|
```
|
311
445
|
|
312
|
-
###
|
446
|
+
### 2.9 target
|
313
447
|
|
314
|
-
If you need to execute some external code in the context of the current state machine
|
448
|
+
If you need to execute some external code in the context of the current state machine, pass that object as a first argument to `new` method.
|
449
|
+
|
450
|
+
Assuming we have a simple `Engine` class that holds an internal state whether the car's engine is on or off:
|
315
451
|
|
316
452
|
```ruby
|
317
|
-
|
453
|
+
class Engine
|
454
|
+
def initialize
|
455
|
+
@engine = false
|
456
|
+
end
|
318
457
|
|
319
|
-
|
320
|
-
|
458
|
+
def turn_on
|
459
|
+
@engine = true
|
460
|
+
end
|
321
461
|
|
322
|
-
|
462
|
+
def turn_off
|
463
|
+
@engine = false
|
464
|
+
end
|
323
465
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
}
|
466
|
+
def engine_on?
|
467
|
+
@engine
|
468
|
+
end
|
328
469
|
end
|
329
470
|
```
|
330
471
|
|
331
|
-
|
472
|
+
And given an instance of `Engine` class:
|
332
473
|
|
333
474
|
```ruby
|
334
|
-
|
475
|
+
engine = Engine.new
|
476
|
+
```
|
335
477
|
|
336
|
-
|
337
|
-
initial :neutral
|
478
|
+
You can provide a context to a state machine by passing it as a first argument to a `new` call. You can then reference this context inside the callbacks by calling the `target` helper:
|
338
479
|
|
339
|
-
|
480
|
+
```ruby
|
481
|
+
fm = FiniteMachine.new(engine) do
|
482
|
+
initial :neutral
|
340
483
|
|
341
|
-
|
342
|
-
|
343
|
-
}
|
484
|
+
event :start, :neutral => :one, unless: "engine_on?"
|
485
|
+
event :stop, :one => :neutral
|
344
486
|
|
345
|
-
|
346
|
-
|
347
|
-
on_exit_start do |event| target.turn_engine_off end
|
348
|
-
}
|
487
|
+
on_before_start { |event| target.turn_on }
|
488
|
+
on_after_stop { |event| target.turn_off }
|
349
489
|
end
|
350
490
|
```
|
351
491
|
|
352
|
-
For more complex example see [Integration](#
|
492
|
+
For more complex example see [Integration](#7-integration) section.
|
493
|
+
|
494
|
+
#### 2.9.1 `:alias_target`
|
353
495
|
|
354
|
-
|
496
|
+
If you wish to better express the intention behind the context object, in particular when calling actions in callbacks, you can use the `:alias_target` option:
|
355
497
|
|
356
498
|
```ruby
|
357
|
-
|
499
|
+
engine = Engine.new
|
358
500
|
|
359
|
-
fm = FiniteMachine.
|
501
|
+
fm = FiniteMachine.new(engine, alias_target: :engine) do
|
360
502
|
initial :neutral
|
361
503
|
|
362
|
-
|
504
|
+
event :start, :neutral => :one, unless: "engine_on?"
|
505
|
+
event :stop, :none => :neutral, if: "engine_on?"
|
363
506
|
|
364
|
-
|
365
|
-
|
366
|
-
}
|
367
|
-
callbacks {
|
368
|
-
on_enter_start do |event|
|
369
|
-
target.turn_engine_on
|
370
|
-
end
|
371
|
-
}
|
507
|
+
on_before_start { |event| engine.turn_on }
|
508
|
+
on_after_stop { |event| engine.turn_off }
|
372
509
|
end
|
373
510
|
```
|
374
511
|
|
375
|
-
|
376
|
-
|
377
|
-
If you need to better express the intention behind the target name, in particular when calling actions in callbacks, you can use the `alias_target` helper:
|
512
|
+
Alternatively, you can use the `alias_target` helper method:
|
378
513
|
|
379
514
|
```ruby
|
380
|
-
|
515
|
+
engine = Engine.new
|
516
|
+
|
517
|
+
Car = FiniteMachine.define do
|
518
|
+
alias_target :engine
|
381
519
|
|
382
|
-
fm = FiniteMachine.define do
|
383
520
|
initial :neutral
|
384
521
|
|
385
|
-
|
522
|
+
event :start, :neutral => :one, if: "engine_on?"
|
523
|
+
event :stop, :none => :neutral, if: "engine_on?"
|
386
524
|
|
387
|
-
|
525
|
+
on_before_start { |event| engine.turn_on }
|
526
|
+
on_after_stop { |event| engine.turn_off }
|
527
|
+
end
|
528
|
+
```
|
388
529
|
|
389
|
-
|
390
|
-
event :start, :neutral => :one, if: "engine_on?"
|
391
|
-
}
|
530
|
+
Then to link `Car` definition with `Engine` instance, pass the `Engine` instance as a first argument:
|
392
531
|
|
393
|
-
|
394
|
-
|
395
|
-
on_exit_start do |event| car.turn_engine_off end
|
396
|
-
}
|
397
|
-
end
|
532
|
+
```ruby
|
533
|
+
car = Car.new(engine)
|
398
534
|
```
|
399
535
|
|
400
|
-
|
536
|
+
Triggering `start` event will change `Engine` instance state from `false` to `true`:
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
engine.engine_on? # => false
|
540
|
+
car.start
|
541
|
+
car.current # => :one
|
542
|
+
engine.engine_on? # => true
|
543
|
+
```
|
544
|
+
|
545
|
+
### 2.10 restore!
|
401
546
|
|
402
547
|
In order to set the machine to a given state and thus skip triggering callbacks use the `restore!` method:
|
403
548
|
|
@@ -407,7 +552,7 @@ fm.restore!(:neutral)
|
|
407
552
|
|
408
553
|
This method may be suitable when used testing your state machine or in restoring the state from datastore.
|
409
554
|
|
410
|
-
###
|
555
|
+
### 2.11 states
|
411
556
|
|
412
557
|
You can use the `states` method to return an array of all the states for a given state machine.
|
413
558
|
|
@@ -415,25 +560,25 @@ You can use the `states` method to return an array of all the states for a given
|
|
415
560
|
fm.states # => [:none, :green, :yellow, :red]
|
416
561
|
```
|
417
562
|
|
418
|
-
###
|
563
|
+
### 2.12 events
|
419
564
|
|
420
|
-
To find out all the event names supported by the state machine issue `
|
565
|
+
To find out all the event names supported by the state machine issue `events` method:
|
421
566
|
|
422
567
|
```ruby
|
423
|
-
fm.
|
568
|
+
fm.events # => [:init, :ready, :go, :stop]
|
424
569
|
```
|
425
570
|
|
426
|
-
##
|
571
|
+
## 3. States and Transitions
|
427
572
|
|
428
|
-
The
|
573
|
+
The **FiniteMachine** DSL exposes the `event` helper to define possible state transitions.
|
429
574
|
|
430
|
-
The `event` helper accepts as a first
|
431
|
-
method on the **FiniteMachine** instance. As a second
|
575
|
+
The `event` helper accepts as a first argument the transition's name which will later be used to create
|
576
|
+
method on the **FiniteMachine** instance. As a second argument the `event` accepts an arbitrary number of states either
|
432
577
|
in the form of `:from` and `:to` hash keys or by using the state names themselves as key value pairs.
|
433
578
|
|
434
579
|
```ruby
|
435
580
|
event :start, from: :neutral, to: :first
|
436
|
-
or
|
581
|
+
# or
|
437
582
|
event :start, :neutral => :first
|
438
583
|
```
|
439
584
|
|
@@ -444,7 +589,9 @@ The following methods trigger transitions for the example state machine.
|
|
444
589
|
* go
|
445
590
|
* stop
|
446
591
|
|
447
|
-
|
592
|
+
You can always opt out from automatic method generation by using [:auto_methods](#271-auto_methods) option.
|
593
|
+
|
594
|
+
### 3.1 Triggering transitions
|
448
595
|
|
449
596
|
In order to transition to the next reachable state, simply call the event's name on the **FiniteMachine** instance. If the transition succeeds the `true` value is returned, otherwise `false`.
|
450
597
|
|
@@ -462,15 +609,15 @@ fm.trigger(:ready) # => true
|
|
462
609
|
Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback as well as used by any present guarding conditions.
|
463
610
|
|
464
611
|
```ruby
|
465
|
-
fm.go(
|
612
|
+
fm.go("Piotr!") # => true
|
466
613
|
fm.current # => :green
|
467
614
|
```
|
468
615
|
|
469
616
|
By default **FiniteMachine** will swallow all exceptions when and return `false` on failure. If you prefer to be notified when illegal transition occurs see [Dangerous transitions](#22-dangerous-transitions).
|
470
617
|
|
471
|
-
###
|
618
|
+
### 3.2 Dangerous transitions
|
472
619
|
|
473
|
-
When you declare event, for instance `ready`, the **FiniteMachine** will provide a dangerous version with a bang `ready!`. In the case when you attempt to perform illegal transition or **FiniteMachine** throws
|
620
|
+
When you declare event, for instance `ready`, the **FiniteMachine** will provide a dangerous version with a bang `ready!`. In the case when you attempt to perform illegal transition or **FiniteMachine** throws internal error, the state machine will propagate the errors. You can use handlers to decide how to handle errors on case by case basis see [6. Error Handling](#6-errors)
|
474
621
|
|
475
622
|
```ruby
|
476
623
|
fm.ready! # => raises FiniteMachine::InvalidStateError
|
@@ -482,63 +629,36 @@ If you prefer you can also use `trigger!` method to fire event:
|
|
482
629
|
fm.trigger!(:ready)
|
483
630
|
```
|
484
631
|
|
485
|
-
###
|
486
|
-
|
487
|
-
By default the transitions will be fired synchronosuly.
|
488
|
-
|
489
|
-
```ruby
|
490
|
-
fm.ready
|
491
|
-
or
|
492
|
-
fm.sync.ready
|
493
|
-
fm.current # => :yellow
|
494
|
-
```
|
495
|
-
|
496
|
-
In order to fire the event transition asynchronously use the `async` scope like so
|
497
|
-
|
498
|
-
```ruby
|
499
|
-
fm.async.ready('Piotr') # => executes in separate Thread
|
500
|
-
```
|
501
|
-
|
502
|
-
The `async` call allows for alternative syntax whereby the method name is passed as one of the parameters like so:
|
503
|
-
|
504
|
-
```ruby
|
505
|
-
fm.async(:ready, 'Piotr')
|
506
|
-
```
|
507
|
-
|
508
|
-
### 2.4 Multiple from states
|
632
|
+
### 3.3 Multiple from states
|
509
633
|
|
510
634
|
If an event transitions from multiple states to the same state then all the states can be grouped into an array.
|
511
635
|
Alternatively, you can create separate events under the same name for each transition that needs combining.
|
512
636
|
|
513
637
|
```ruby
|
514
|
-
fm = FiniteMachine.
|
638
|
+
fm = FiniteMachine.new do
|
515
639
|
initial :neutral
|
516
640
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
event :slow, [:one, :two, :three] => :one
|
523
|
-
}
|
641
|
+
event :start, :neutral => :one
|
642
|
+
event :shift, :one => :two
|
643
|
+
event :shift, :two => :three
|
644
|
+
event :shift, :three => :four
|
645
|
+
event :slow, [:one, :two, :three] => :one
|
524
646
|
end
|
525
647
|
```
|
526
648
|
|
527
|
-
###
|
649
|
+
### 3.4 `any_state` transitions
|
528
650
|
|
529
|
-
The **FiniteMachine** offers few ways to transition out of any state. This is
|
651
|
+
The **FiniteMachine** offers few ways to transition out of any state. This is particularly useful when the machine already defines many states.
|
530
652
|
|
531
|
-
You can
|
653
|
+
You can use `any_state` as the name for a given state, for instance:
|
532
654
|
|
533
655
|
```ruby
|
534
|
-
event :run, from:
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
event :run, :any => :green
|
656
|
+
event :run, from: any_state, to: :green
|
657
|
+
# or
|
658
|
+
event :run, any_state => :green
|
539
659
|
```
|
540
660
|
|
541
|
-
Alternatively, you can skip the
|
661
|
+
Alternatively, you can skip the `any_state` call and just specify `to` state:
|
542
662
|
|
543
663
|
```ruby
|
544
664
|
event :run, to: :green
|
@@ -546,78 +666,73 @@ event :run, to: :green
|
|
546
666
|
|
547
667
|
All the above `run` event definitions will always transition the state machine into `:green` state.
|
548
668
|
|
549
|
-
###
|
669
|
+
### 3.5 Collapsing transitions
|
550
670
|
|
551
671
|
Another way to specify state transitions under single event name is to group all your state transitions into a single hash like so:
|
552
672
|
|
553
673
|
```ruby
|
554
|
-
fm = FiniteMachine.
|
674
|
+
fm = FiniteMachine.new do
|
555
675
|
initial :initial
|
556
676
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
}
|
677
|
+
event :bump, :initial => :low,
|
678
|
+
:low => :medium,
|
679
|
+
:medium => :high
|
680
|
+
end
|
562
681
|
```
|
563
682
|
|
564
683
|
The same can be more naturally rewritten also as:
|
565
684
|
|
566
685
|
```ruby
|
567
|
-
fm = FiniteMachine.
|
686
|
+
fm = FiniteMachine.new do
|
568
687
|
initial :initial
|
569
688
|
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
}
|
689
|
+
event :bump, :initial => :low
|
690
|
+
event :bump, :low => :medium
|
691
|
+
event :bump, :medium => :high
|
692
|
+
end
|
575
693
|
```
|
576
694
|
|
577
|
-
###
|
695
|
+
### 3.6 Silent transitions
|
578
696
|
|
579
697
|
The **FiniteMachine** allows to selectively silence events and thus prevent any callbacks from firing. Using the `silent` option passed to event definition like so:
|
580
698
|
|
581
699
|
```ruby
|
582
|
-
fm = FiniteMachine.
|
700
|
+
fm = FiniteMachine.new do
|
583
701
|
initial :yellow
|
584
702
|
|
585
|
-
|
586
|
-
|
587
|
-
event :stop, :green => :red
|
588
|
-
}
|
703
|
+
event :go :yellow => :green, silent: true
|
704
|
+
event :stop, :green => :red
|
589
705
|
end
|
590
706
|
|
591
|
-
|
592
|
-
|
707
|
+
fm.go # no callbacks
|
708
|
+
fm.stop # callbacks are fired
|
593
709
|
```
|
594
710
|
|
595
|
-
###
|
711
|
+
### 3.7 Logging transitions
|
596
712
|
|
597
713
|
To help debug your state machine, **FiniteMachine** provides `:log_transitions` option.
|
598
714
|
|
599
715
|
```ruby
|
600
|
-
FiniteMachine.
|
716
|
+
FiniteMachine.new(log_transitions: true) do
|
601
717
|
...
|
602
718
|
end
|
603
719
|
```
|
604
720
|
|
605
|
-
|
721
|
+
### 3.8 Conditional transitions
|
606
722
|
|
607
723
|
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.
|
608
724
|
|
609
|
-
|
725
|
+
#### 3.8.1 Using a Proc
|
610
726
|
|
611
727
|
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.
|
612
728
|
|
613
729
|
```ruby
|
614
|
-
fm = FiniteMachine.
|
730
|
+
fm = FiniteMachine.new do
|
615
731
|
initial :green
|
616
732
|
|
617
|
-
|
618
|
-
event :slow, :green => :yellow, if: -> { return false }
|
619
|
-
}
|
733
|
+
event :slow, :green => :yellow, if: -> { return false }
|
620
734
|
end
|
735
|
+
|
621
736
|
fm.slow # doesn't transition to :yellow state
|
622
737
|
fm.current # => :green
|
623
738
|
```
|
@@ -625,14 +740,13 @@ fm.current # => :green
|
|
625
740
|
Condition by default receives the current context, which is the current state machine instance, followed by extra arguments.
|
626
741
|
|
627
742
|
```ruby
|
628
|
-
|
743
|
+
fm = FiniteMachine.new do
|
629
744
|
initial :red
|
630
745
|
|
631
|
-
|
632
|
-
|
633
|
-
if: -> (context, a) { context.current == a }
|
634
|
-
}
|
746
|
+
event :go, :red => :green,
|
747
|
+
if: ->(context, a) { context.current == a }
|
635
748
|
end
|
749
|
+
|
636
750
|
fm.go(:yellow) # doesn't transition
|
637
751
|
fm.go # raises ArgumentError
|
638
752
|
```
|
@@ -640,263 +754,235 @@ fm.go # raises ArgumentError
|
|
640
754
|
**Note** If you specify condition with a given number of arguments then you need to call an event with the exact number of arguments, otherwise you will get `ArgumentError`. Thus in above scenario to prevent errors specify condition like so:
|
641
755
|
|
642
756
|
```ruby
|
643
|
-
if: ->
|
757
|
+
if: ->(context, *args) { ... }
|
644
758
|
```
|
645
759
|
|
646
760
|
Provided your **FiniteMachine** is associated with another object through `target` helper. Then the target object together with event arguments will be passed to the `:if` or `:unless` condition scope.
|
647
761
|
|
648
762
|
```ruby
|
649
|
-
class
|
650
|
-
|
763
|
+
class Engine
|
764
|
+
def initialize
|
765
|
+
@engine = false
|
766
|
+
end
|
651
767
|
|
652
|
-
def
|
653
|
-
@
|
768
|
+
def turn_on
|
769
|
+
@engine = true
|
654
770
|
end
|
655
771
|
|
656
|
-
def
|
657
|
-
@
|
772
|
+
def turn_off
|
773
|
+
@engine = false
|
658
774
|
end
|
659
775
|
|
660
776
|
def engine_on?
|
661
|
-
@
|
777
|
+
@engine
|
662
778
|
end
|
663
779
|
end
|
664
780
|
|
665
|
-
|
666
|
-
|
781
|
+
engine = Engine.new
|
782
|
+
engine.turn_on
|
667
783
|
|
668
|
-
|
784
|
+
car = FiniteMachine.new(engine) do
|
669
785
|
initial :neutral
|
670
786
|
|
671
|
-
target
|
672
|
-
|
673
|
-
|
674
|
-
event :start, :neutral => :one, if: -> (target, state) {
|
675
|
-
target.engine_on = state
|
676
|
-
target.engine_on?
|
677
|
-
}
|
678
|
-
}
|
787
|
+
event :start, :neutral => :one, if: ->(target, state) do
|
788
|
+
state ? target.engine_on : target.engine_off
|
789
|
+
end
|
679
790
|
end
|
680
791
|
|
681
792
|
fm.start(false)
|
682
|
-
fm.current
|
793
|
+
fm.current # => :neutral
|
794
|
+
engine.engine_on? # => false
|
795
|
+
|
683
796
|
fm.start(true)
|
684
|
-
fm.current
|
797
|
+
fm.current # => :one
|
798
|
+
engine.engine_on? # => true
|
685
799
|
```
|
686
800
|
|
687
|
-
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [
|
801
|
+
When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See [4.9 Cancelling callbacks](#49-cancelling-inside-callbacks)
|
688
802
|
|
689
|
-
|
803
|
+
#### 3.8.2 Using a Symbol
|
690
804
|
|
691
805
|
You can also use a symbol corresponding to the name of a method that will get called right before transition happens.
|
692
806
|
|
693
807
|
```ruby
|
694
|
-
|
808
|
+
fm = FiniteMachine.new(engine) do
|
695
809
|
initial :neutral
|
696
810
|
|
697
|
-
|
698
|
-
|
699
|
-
events {
|
700
|
-
event :start, :neutral => :one, if: :engine_on?
|
701
|
-
}
|
811
|
+
event :start, :neutral => :one, if: :engine_on?
|
702
812
|
end
|
703
813
|
```
|
704
814
|
|
705
|
-
|
815
|
+
#### 3.8.3 Using a String
|
706
816
|
|
707
817
|
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.
|
708
818
|
|
709
819
|
```ruby
|
710
|
-
|
820
|
+
fm = FiniteMachine.new(engine) do
|
711
821
|
initial :neutral
|
712
822
|
|
713
|
-
|
714
|
-
|
715
|
-
events {
|
716
|
-
event :start, :neutral => :one, if: "engine_on?"
|
717
|
-
}
|
823
|
+
event :start, :neutral => :one, if: "engine_on?"
|
718
824
|
end
|
719
825
|
```
|
720
826
|
|
721
|
-
|
827
|
+
#### 3.8.4 Combining transition conditions
|
722
828
|
|
723
829
|
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.
|
724
830
|
|
725
831
|
```ruby
|
726
|
-
|
832
|
+
fm = FiniteMachine.new do
|
727
833
|
initial :green
|
728
834
|
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
event :stop, :yellow => :red
|
734
|
-
}
|
835
|
+
event :slow, :green => :yellow,
|
836
|
+
if: [ -> { return true }, -> { return true} ],
|
837
|
+
unless: -> { return true }
|
838
|
+
event :stop, :yellow => :red
|
735
839
|
end
|
736
840
|
```
|
737
841
|
|
738
842
|
The transition only runs when all the `:if` conditions and none of the `unless` conditions are evaluated to `true`.
|
739
843
|
|
740
|
-
|
844
|
+
### 3.9 Choice pseudostates
|
741
845
|
|
742
|
-
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to
|
846
|
+
Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to select only one outgoing transition.
|
743
847
|
|
744
848
|
You can implement the conditional branch as ordinary events grouped under the same name and use familiar `:if/:unless` conditions:
|
745
849
|
|
746
850
|
```ruby
|
747
|
-
|
851
|
+
fm = FiniteMachine.define do
|
748
852
|
initial :green
|
749
853
|
|
750
|
-
|
751
|
-
|
752
|
-
event :next, :green => :red, if: -> { true }
|
753
|
-
}
|
854
|
+
event :next, :green => :yellow, if: -> { false }
|
855
|
+
event :next, :green => :red, if: -> { true }
|
754
856
|
end
|
755
857
|
|
756
|
-
|
757
|
-
|
758
|
-
|
858
|
+
fm.current # => :green
|
859
|
+
fm.next
|
860
|
+
fm.current # => :red
|
759
861
|
```
|
760
862
|
|
761
863
|
The same conditional logic can be implemented using much shorter and more descriptive style using `choice` method:
|
762
864
|
|
763
865
|
```ruby
|
764
|
-
|
866
|
+
fm = FiniteMachine.new do
|
765
867
|
initial :green
|
766
868
|
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
end
|
772
|
-
}
|
869
|
+
event :next, from: :green do
|
870
|
+
choice :yellow, if: -> { false }
|
871
|
+
choice :red, if: -> { true }
|
872
|
+
end
|
773
873
|
end
|
774
874
|
|
775
|
-
|
776
|
-
|
777
|
-
|
875
|
+
fm.current # => :green
|
876
|
+
fm.next
|
877
|
+
fm.current # => :red
|
778
878
|
```
|
779
879
|
|
780
|
-
|
880
|
+
#### 3.9.1 Dynamic choice conditions
|
781
881
|
|
782
882
|
Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:
|
783
883
|
|
784
884
|
```ruby
|
785
|
-
|
885
|
+
fm = FiniteMachine.new do
|
786
886
|
initial :green
|
787
887
|
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
end
|
794
|
-
}
|
888
|
+
event :next, from: :green do
|
889
|
+
choice :yellow, if: ->(context, a) { a < 1 }
|
890
|
+
choice :red, if: ->(context, a) { a > 1 }
|
891
|
+
default :red
|
892
|
+
end
|
795
893
|
end
|
796
894
|
|
797
|
-
|
798
|
-
|
799
|
-
|
895
|
+
fm.current # => :green
|
896
|
+
fm.next(0)
|
897
|
+
fm.current # => :yellow
|
800
898
|
```
|
801
899
|
|
802
900
|
If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the `default` state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify `default` choice.
|
803
901
|
|
804
|
-
|
902
|
+
#### 3.9.2 Multiple from states
|
805
903
|
|
806
904
|
Similarly to event definitions, you can specify the event to transition from a group of states:
|
807
905
|
|
808
906
|
```ruby
|
809
|
-
FiniteMachine.
|
907
|
+
FiniteMachine.new do
|
810
908
|
initial :red
|
811
909
|
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
end
|
817
|
-
}
|
910
|
+
event :next, from: [:yellow, :red] do
|
911
|
+
choice :pink, if: -> { false }
|
912
|
+
choice :green
|
913
|
+
end
|
818
914
|
end
|
819
915
|
```
|
820
916
|
|
821
|
-
|
917
|
+
Or from any state using the `:any` state name like so:
|
822
918
|
|
823
919
|
```ruby
|
824
|
-
FiniteMachine.
|
920
|
+
FiniteMachine.new do
|
825
921
|
initial :red
|
826
922
|
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
end
|
832
|
-
}
|
923
|
+
event :next, from: :any do
|
924
|
+
choice :pink, if: -> { false }
|
925
|
+
choice :green
|
926
|
+
end
|
833
927
|
end
|
834
928
|
```
|
835
929
|
|
836
|
-
##
|
837
|
-
|
838
|
-
You can watch state machine events and the information they provide by registering one or more predefined callback types. The following 5 types of callbacks are available in **FiniteMachine**:
|
930
|
+
## 4. Callbacks
|
839
931
|
|
840
|
-
|
841
|
-
* `on_transition`
|
842
|
-
* `on_exit`
|
843
|
-
* `on_before`
|
844
|
-
* `on_after`
|
932
|
+
You can register a callback to listen for state transitions and events triggered, and based on these perform custom actions. There are five callbacks available in **FiniteMachine**:
|
845
933
|
|
846
|
-
|
934
|
+
* `on_before` - triggered before any transition
|
935
|
+
* `on_exit` - triggered when leaving any state
|
936
|
+
* `on_transition` - triggered during any transition
|
937
|
+
* `on_enter` - triggered when entering any state
|
938
|
+
* `on_after` - triggered after any transition
|
847
939
|
|
848
940
|
Use the state or event name as a first parameter to the callback helper followed by block with event argument and a list arguments that you expect to receive like so:
|
849
941
|
|
850
942
|
```ruby
|
851
|
-
on_enter
|
943
|
+
on_enter(:green) { |event, a, b, c| ... }
|
852
944
|
```
|
853
945
|
|
854
946
|
When you subscribe to the `:green` state change, the callback will be called whenever someone triggers event that transitions in or out of that state. The same will happen on subscription to event `ready`, namely, the callback will be called each time the state transition method is triggered regardless of the states it transitions from or to.
|
855
947
|
|
856
948
|
```ruby
|
857
|
-
fm = FiniteMachine.
|
949
|
+
fm = FiniteMachine.new do
|
858
950
|
initial :red
|
859
951
|
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
}
|
952
|
+
event :ready, :red => :yellow
|
953
|
+
event :go, :yellow => :green
|
954
|
+
event :stop, :green => :red
|
955
|
+
|
956
|
+
on_before :ready do |event, time1, time2, time3|
|
957
|
+
puts "#{time1} #{time2} #{time3} Go!" }
|
958
|
+
end
|
959
|
+
on_before :go do |event, name|
|
960
|
+
puts "Going fast #{name}"
|
961
|
+
end
|
962
|
+
on_before(:stop) { |event| ... }
|
871
963
|
end
|
872
964
|
|
873
965
|
fm.ready(1, 2, 3)
|
874
|
-
fm.go(
|
966
|
+
fm.go("Piotr!")
|
875
967
|
```
|
876
968
|
|
877
969
|
**Note** Regardless of how the state is entered or exited, all the associated callbacks will be executed. This provides means for guaranteed initialization and cleanup.
|
878
970
|
|
879
|
-
###
|
971
|
+
### 4.1 on_(enter|transition|exit)
|
880
972
|
|
881
973
|
The `on_enter` callback is executed before given state change is fired. By passing state name you can narrow down the listener to only watch out for enter state changes. Otherwise, all enter state changes will be watched.
|
882
974
|
|
883
|
-
### 5.2 on_transition
|
884
|
-
|
885
975
|
The `on_transition` callback is executed when given state change happens. By passing state name you can narrow down the listener to only watch out for transition state changes. Otherwise, all transition state changes will be watched.
|
886
976
|
|
887
|
-
### 5.3 on_exit
|
888
|
-
|
889
977
|
The `on_exit` callback is executed after a given state change happens. By passing state name you can narrow down the listener to only watch out for exit state changes. Otherwise, all exit state changes will be watched.
|
890
978
|
|
891
|
-
###
|
979
|
+
### 4.2 on_(before|after)
|
892
980
|
|
893
981
|
The `on_before` callback is executed before a given event happens. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
|
894
982
|
|
895
|
-
### 5.5 on_after
|
896
|
-
|
897
983
|
This callback is executed after a given event happened. By default it will listen out for all events, you can also listen out for specific events by passing event's name.
|
898
984
|
|
899
|
-
###
|
985
|
+
### 4.3 once_on
|
900
986
|
|
901
987
|
**FiniteMachine** allows you to listen on initial state change or when the event is fired first time by using the following 5 types of callbacks:
|
902
988
|
|
@@ -906,7 +992,7 @@ This callback is executed after a given event happened. By default it will liste
|
|
906
992
|
* `once_before`
|
907
993
|
* `once_after`
|
908
994
|
|
909
|
-
###
|
995
|
+
### 4.4 Execution sequence
|
910
996
|
|
911
997
|
Assuming we have the following event specified:
|
912
998
|
|
@@ -914,92 +1000,90 @@ Assuming we have the following event specified:
|
|
914
1000
|
event :go, :red => :yellow
|
915
1001
|
```
|
916
1002
|
|
917
|
-
|
1003
|
+
Then by calling `go` event the following callbacks sequence will be executed:
|
918
1004
|
|
919
1005
|
* `on_before` - generic callback before `any` event
|
920
1006
|
* `on_before :go` - callback before the `go` event
|
921
|
-
* `on_exit :red` - callback for the `:red` state exit
|
922
1007
|
* `on_exit` - generic callback for exit from `any` state
|
923
|
-
* `
|
1008
|
+
* `on_exit :red` - callback for the `:red` state exit
|
924
1009
|
* `on_transition` - callback for transition from `any` state to `any` state
|
925
|
-
* `
|
1010
|
+
* `on_transition :yellow` - callback for the `:red` to `:yellow` transition
|
926
1011
|
* `on_enter` - generic callback for entry to `any` state
|
927
|
-
* `
|
1012
|
+
* `on_enter :yellow` - callback for the `:yellow` state entry
|
928
1013
|
* `on_after` - generic callback after `any` event
|
1014
|
+
* `on_after :go` - callback after the `go` event
|
929
1015
|
|
930
|
-
### 5
|
1016
|
+
### 4.5 Callback parameters
|
931
1017
|
|
932
|
-
All callbacks
|
1018
|
+
All callbacks as a first argument yielded to a block receive the `TransitionEvent` object with the following attributes:
|
933
1019
|
|
934
|
-
* `name
|
935
|
-
* `from
|
936
|
-
* `to
|
1020
|
+
* `name` - the event name`
|
1021
|
+
* `from` - the state transitioning from`
|
1022
|
+
* `to` - the state transitioning to`
|
937
1023
|
|
938
1024
|
followed by the rest of arguments that were passed to the event method.
|
939
1025
|
|
940
1026
|
```ruby
|
941
|
-
fm = FiniteMachine.
|
1027
|
+
fm = FiniteMachine.new do
|
942
1028
|
initial :red
|
943
1029
|
|
944
|
-
|
945
|
-
event :ready, :red => :yellow
|
946
|
-
}
|
1030
|
+
event :ready, :red => :yellow
|
947
1031
|
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
}
|
952
|
-
}
|
1032
|
+
on_before_ready do |event, time|
|
1033
|
+
puts "lights switching from #{event.from} to #{event.to} in #{time} seconds"
|
1034
|
+
end
|
953
1035
|
end
|
954
1036
|
|
955
|
-
fm.ready(3)
|
1037
|
+
fm.ready(3)
|
1038
|
+
# => "lights switching from red to yellow in 3 seconds"
|
956
1039
|
```
|
957
1040
|
|
958
|
-
###
|
1041
|
+
### 4.6 Duplicate callbacks
|
959
1042
|
|
960
1043
|
You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.
|
961
1044
|
|
1045
|
+
Given the following state machine instance:
|
1046
|
+
|
962
1047
|
```ruby
|
963
|
-
fm = FiniteMachine.
|
1048
|
+
fm = FiniteMachine.new do
|
964
1049
|
initial :green
|
965
1050
|
|
966
|
-
|
967
|
-
event :slow, :green => :yellow
|
968
|
-
}
|
1051
|
+
event :slow, :green => :yellow
|
969
1052
|
|
970
|
-
|
971
|
-
|
972
|
-
on_enter(:yellow) { then_this }
|
973
|
-
}
|
1053
|
+
on_enter(:yellow) { puts "this is run first" }
|
1054
|
+
on_enter(:yellow) { puts "then this is run" }
|
974
1055
|
end
|
975
|
-
fm.slow # => will invoke both callbacks
|
976
1056
|
```
|
977
1057
|
|
978
|
-
|
1058
|
+
Triggerring the `:slow` event results in:
|
1059
|
+
|
1060
|
+
```ruby
|
1061
|
+
fm.slow
|
1062
|
+
# => "this is run first"
|
1063
|
+
# => "then this is run"
|
1064
|
+
```
|
1065
|
+
|
1066
|
+
### 4.7 Fluid callbacks
|
979
1067
|
|
980
|
-
Callbacks can also be specified as full method calls
|
1068
|
+
Callbacks can also be specified as full method calls separated with underscores:
|
981
1069
|
|
982
1070
|
```ruby
|
983
1071
|
fm = FiniteMachine.define do
|
984
1072
|
initial :red
|
985
1073
|
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
}
|
991
|
-
|
992
|
-
|
993
|
-
on_before_ready { |event| ... }
|
994
|
-
on_before_go { |event| ... }
|
995
|
-
on_before_stop { |event| ... }
|
996
|
-
}
|
1074
|
+
event :ready, :red => :yellow
|
1075
|
+
event :go, :yellow => :green
|
1076
|
+
event :stop, :green => :red
|
1077
|
+
|
1078
|
+
on_before_ready { |event| ... }
|
1079
|
+
on_before_go { |event| ... }
|
1080
|
+
on_before_stop { |event| ... }
|
997
1081
|
end
|
998
1082
|
```
|
999
1083
|
|
1000
|
-
###
|
1084
|
+
### 4.8 Methods inside callbacks
|
1001
1085
|
|
1002
|
-
|
1086
|
+
Given a class `Car`:
|
1003
1087
|
|
1004
1088
|
```ruby
|
1005
1089
|
class Car
|
@@ -1013,111 +1097,118 @@ class Car
|
|
1013
1097
|
@reverse_lights = true
|
1014
1098
|
end
|
1015
1099
|
end
|
1100
|
+
```
|
1016
1101
|
|
1102
|
+
We can easily manipulate state for an instance of a `Car` class:
|
1103
|
+
|
1104
|
+
```ruby
|
1017
1105
|
car = Car.new
|
1106
|
+
```
|
1018
1107
|
|
1019
|
-
|
1020
|
-
initial :neutral
|
1108
|
+
By defining finite machine using the instance:
|
1021
1109
|
|
1022
|
-
|
1110
|
+
```ruby
|
1111
|
+
fm = FiniteMachine.new(car) do
|
1112
|
+
initial :neutral
|
1023
1113
|
|
1024
|
-
|
1025
|
-
|
1026
|
-
event :back, [:neutral, :one] => :reverse
|
1027
|
-
}
|
1114
|
+
event :forward, [:reverse, :neutral] => :one
|
1115
|
+
event :back, [:neutral, :one] => :reverse
|
1028
1116
|
|
1029
|
-
|
1030
|
-
|
1031
|
-
on_exit_reverse { |event| target.turn_reverse_lights_off }
|
1032
|
-
}
|
1117
|
+
on_enter_reverse { |event| target.turn_reverse_lights_on }
|
1118
|
+
on_exit_reverse { |event| target.turn_reverse_lights_off }
|
1033
1119
|
end
|
1034
1120
|
```
|
1035
1121
|
|
1036
1122
|
Note that you can also fire events from callbacks.
|
1037
1123
|
|
1038
1124
|
```ruby
|
1039
|
-
fm = FiniteMachine.
|
1125
|
+
fm = FiniteMachine.new do
|
1040
1126
|
initial :neutral
|
1041
1127
|
|
1042
|
-
|
1043
|
-
|
1044
|
-
event :back, [:neutral, :one] => :reverse
|
1045
|
-
}
|
1128
|
+
event :forward, [:reverse, :neutral] => :one
|
1129
|
+
event :back, [:neutral, :one] => :reverse
|
1046
1130
|
|
1047
|
-
|
1048
|
-
|
1049
|
-
on_exit_reverse { |event, name| puts "Go #{name}" }
|
1050
|
-
}
|
1131
|
+
on_enter_reverse { |event| forward("Piotr!") }
|
1132
|
+
on_exit_reverse { |event, name| puts "Go #{name}" }
|
1051
1133
|
end
|
1052
|
-
fm.back # => Go Piotr!
|
1053
1134
|
```
|
1054
1135
|
|
1055
|
-
|
1136
|
+
Then triggerring `:back` event gives:
|
1056
1137
|
|
1057
|
-
|
1138
|
+
```ruby
|
1139
|
+
fm.back # => Go Piotr!
|
1140
|
+
```
|
1141
|
+
|
1142
|
+
For more complex example see [Integration](#7-integration) section.
|
1143
|
+
|
1144
|
+
### 4.9 Cancelling callbacks
|
1058
1145
|
|
1059
|
-
|
1146
|
+
A simple way to prevent transitions is to use [3 Conditional transitions](#3-conditional-transitions).
|
1147
|
+
|
1148
|
+
There are times when you want to cancel transition in a callback. For example, you have logic which allows transition to happen only under certain complex conditions. Using `cancel_event` inside the `on_(enter|transition|exit)` or `on_(before|after)` callbacks will stop all the callbacks from firing and prevent current transition from happening.
|
1149
|
+
|
1150
|
+
For example, the following state machine cancels any event leaving `:red` state:
|
1060
1151
|
|
1061
1152
|
```ruby
|
1062
|
-
fm = FiniteMachine.
|
1153
|
+
fm = FiniteMachine.new do
|
1063
1154
|
initial :red
|
1064
1155
|
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
event :stop, :green => :red
|
1069
|
-
}
|
1070
|
-
end
|
1156
|
+
event :ready, :red => :yellow
|
1157
|
+
event :go, :yellow => :green
|
1158
|
+
event :stop, :green => :red
|
1071
1159
|
|
1072
|
-
|
1073
|
-
|
1160
|
+
on_exit :red do |event|
|
1161
|
+
...
|
1162
|
+
cancel_event
|
1163
|
+
end
|
1074
1164
|
end
|
1075
1165
|
```
|
1076
1166
|
|
1077
|
-
|
1078
|
-
|
1079
|
-
By default all callbacks are run synchronosuly. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
|
1167
|
+
Then firing `:ready` event will not transition out of the current `:red` state:
|
1080
1168
|
|
1081
1169
|
```ruby
|
1082
|
-
|
1170
|
+
fm.current # => :red
|
1171
|
+
fm.ready
|
1172
|
+
fm.current # => :red
|
1083
1173
|
```
|
1084
1174
|
|
1085
|
-
|
1175
|
+
### 4.10 Asynchronous callbacks
|
1176
|
+
|
1177
|
+
By default all callbacks are run synchronously. In order to add a callback that runs asynchronously, you need to pass second `:async` argument like so:
|
1086
1178
|
|
1087
1179
|
```ruby
|
1088
|
-
|
1180
|
+
on_enter(:green, :async) do |event| ... end
|
1181
|
+
# or
|
1182
|
+
on_enter_green(:async) { |event| }
|
1089
1183
|
```
|
1090
1184
|
|
1091
|
-
This will ensure that when the callback is fired it will run in
|
1092
|
-
|
1093
|
-
### 5.14 Cancelling inside callbacks
|
1185
|
+
This will ensure that when the callback is fired it will run in separate thread outside of the main execution thread.
|
1094
1186
|
|
1095
|
-
|
1187
|
+
### 4.11 Instance callbacks
|
1096
1188
|
|
1097
|
-
|
1098
|
-
* `on_before :event_name`
|
1189
|
+
When defining callbacks you are not limited to the **FiniteMachine** block definition. After creating an instance, 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.
|
1099
1190
|
|
1100
|
-
For example
|
1191
|
+
For example, given the following state machine:
|
1101
1192
|
|
1102
1193
|
```ruby
|
1103
|
-
fm = FiniteMachine.
|
1194
|
+
fm = FiniteMachine.new do
|
1104
1195
|
initial :red
|
1105
1196
|
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
event :stop, :green => :red
|
1110
|
-
}
|
1111
|
-
callbacks {
|
1112
|
-
on_exit :red do |event| FiniteMachine::CANCELLED end
|
1113
|
-
}
|
1197
|
+
event :ready, :red => :yellow
|
1198
|
+
event :go, :yellow => :green
|
1199
|
+
event :stop, :green => :red
|
1114
1200
|
end
|
1201
|
+
```
|
1115
1202
|
|
1116
|
-
|
1117
|
-
|
1203
|
+
We can add callbacks as follows:
|
1204
|
+
|
1205
|
+
```ruby
|
1206
|
+
fm.on_enter(:yellow) { |event| ... }
|
1207
|
+
# or
|
1208
|
+
fm.en_enter_yellow { |event| ... }
|
1118
1209
|
```
|
1119
1210
|
|
1120
|
-
##
|
1211
|
+
## 5. Error Handling
|
1121
1212
|
|
1122
1213
|
By default, the **FiniteMachine** will throw an exception whenever the machine is in invalid state or fails to transition.
|
1123
1214
|
|
@@ -1125,31 +1216,27 @@ By default, the **FiniteMachine** will throw an exception whenever the machine i
|
|
1125
1216
|
* `FiniteMachine::InvalidStateError`
|
1126
1217
|
* `FiniteMachine::InvalidCallbackError`
|
1127
1218
|
|
1128
|
-
You can attach specific error handler
|
1219
|
+
You can attach specific error handler using the 'handle' with the name of the error as a first argument and a callback to be executed when the error happens. 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.
|
1129
1220
|
|
1130
1221
|
```ruby
|
1131
|
-
fm = FiniteMachine.
|
1222
|
+
fm = FiniteMachine.new do
|
1132
1223
|
initial :green, event: :start
|
1133
1224
|
|
1134
|
-
|
1135
|
-
|
1136
|
-
event :stop, :yellow => :red
|
1137
|
-
}
|
1225
|
+
event :slow, :green => :yellow
|
1226
|
+
event :stop, :yellow => :red
|
1138
1227
|
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
end
|
1228
|
+
handle FiniteMachine::InvalidStateError do |exception|
|
1229
|
+
# run some custom logging
|
1230
|
+
raise exception
|
1231
|
+
end
|
1144
1232
|
|
1145
|
-
|
1146
|
-
}
|
1233
|
+
handle FiniteMachine::TransitionError, with: -> { |exception| ... }
|
1147
1234
|
end
|
1148
1235
|
```
|
1149
1236
|
|
1150
|
-
###
|
1237
|
+
### 5.1 Using target
|
1151
1238
|
|
1152
|
-
You can pass an external context
|
1239
|
+
You can pass an external context as a first argument to the **FiniteMachine** initialization that will be available as context in the handler block or `:with` value. For example, the `log_error` method is made available when `:with` option key is used:
|
1153
1240
|
|
1154
1241
|
```ruby
|
1155
1242
|
class Logger
|
@@ -1158,27 +1245,21 @@ class Logger
|
|
1158
1245
|
end
|
1159
1246
|
end
|
1160
1247
|
|
1161
|
-
fm = FiniteMachine.
|
1162
|
-
target logger
|
1163
|
-
|
1248
|
+
fm = FiniteMachine.new(logger) do
|
1164
1249
|
initial :green
|
1165
1250
|
|
1166
|
-
|
1167
|
-
|
1168
|
-
event :stop, :yellow => :red
|
1169
|
-
}
|
1251
|
+
event :slow, :green => :yellow
|
1252
|
+
event :stop, :yellow => :red
|
1170
1253
|
|
1171
|
-
|
1172
|
-
handle 'InvalidStateError', with: :log_error
|
1173
|
-
}
|
1254
|
+
handle "InvalidStateError", with: :log_error
|
1174
1255
|
end
|
1175
1256
|
```
|
1176
1257
|
|
1177
|
-
##
|
1258
|
+
## 6. Stand-alone
|
1178
1259
|
|
1179
|
-
**FiniteMachine** allows you to
|
1260
|
+
**FiniteMachine** allows you to separate your state machine from the target class so that you can keep your concerns broken in small maintainable pieces.
|
1180
1261
|
|
1181
|
-
###
|
1262
|
+
### 6.1 Creating a Definition
|
1182
1263
|
|
1183
1264
|
You can turn a class into a **FiniteMachine** by simply subclassing `FiniteMachine::Definition`. As a rule of thumb, every single public method of the **FiniteMachine** is available inside your class:
|
1184
1265
|
|
@@ -1186,31 +1267,29 @@ You can turn a class into a **FiniteMachine** by simply subclassing `FiniteMachi
|
|
1186
1267
|
class Engine < FiniteMachine::Definition
|
1187
1268
|
initial :neutral
|
1188
1269
|
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
event :back, [:neutral, :one] => :reverse
|
1193
|
-
}
|
1270
|
+
event :forward, [:reverse, :neutral] => :one
|
1271
|
+
event :shift, :one => :two
|
1272
|
+
event :back, [:neutral, :one] => :reverse
|
1194
1273
|
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
end
|
1274
|
+
on_enter :reverse do |event|
|
1275
|
+
target.turn_reverse_lights_on
|
1276
|
+
end
|
1199
1277
|
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
}
|
1278
|
+
on_exit :reverse do |event|
|
1279
|
+
target.turn_reverse_lights_off
|
1280
|
+
end
|
1204
1281
|
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1282
|
+
handle FiniteMachine::InvalidStateError do |exception|
|
1283
|
+
...
|
1284
|
+
end
|
1208
1285
|
end
|
1209
1286
|
```
|
1210
1287
|
|
1211
|
-
###
|
1288
|
+
### 6.2 Targeting definition
|
1212
1289
|
|
1213
|
-
The next step is to instantiate your state machine and use
|
1290
|
+
The next step is to instantiate your state machine and use a custom class instance to load specific context.
|
1291
|
+
|
1292
|
+
For example, having the following `Car` class:
|
1214
1293
|
|
1215
1294
|
```ruby
|
1216
1295
|
class Car
|
@@ -1227,32 +1306,31 @@ class Car
|
|
1227
1306
|
end
|
1228
1307
|
end
|
1229
1308
|
```
|
1309
|
+
|
1230
1310
|
Thus, to associate `Engine` to `Car` do:
|
1231
1311
|
|
1232
1312
|
```ruby
|
1233
1313
|
car = Car.new
|
1234
|
-
engine = Engine.new
|
1235
|
-
engine.target car
|
1314
|
+
engine = Engine.new(car)
|
1236
1315
|
|
1237
1316
|
car.reverse_lignts? # => false
|
1238
1317
|
engine.back
|
1239
1318
|
car.reverse_lights? # => true
|
1240
1319
|
```
|
1241
1320
|
|
1242
|
-
Alternatively, create method inside the `Car` that will do the integration like so
|
1321
|
+
Alternatively, create method inside the `Car` that will do the integration like so:
|
1243
1322
|
|
1244
1323
|
```ruby
|
1245
1324
|
class Car
|
1246
1325
|
... # as above
|
1326
|
+
|
1247
1327
|
def engine
|
1248
|
-
@engine ||= Engine.new
|
1249
|
-
@engine.target(self)
|
1250
|
-
@engine
|
1328
|
+
@engine ||= Engine.new(self)
|
1251
1329
|
end
|
1252
1330
|
end
|
1253
1331
|
```
|
1254
1332
|
|
1255
|
-
###
|
1333
|
+
### 6.3 Definition inheritance
|
1256
1334
|
|
1257
1335
|
You can create more specialised versions of a generic definition by using inheritance. Assuming a generic state machine definition:
|
1258
1336
|
|
@@ -1260,27 +1338,19 @@ You can create more specialised versions of a generic definition by using inheri
|
|
1260
1338
|
class GenericStateMachine < FiniteMachine::Definition
|
1261
1339
|
initial :red
|
1262
1340
|
|
1263
|
-
|
1264
|
-
event :start, :red => :green
|
1265
|
-
}
|
1341
|
+
event :start, :red => :green
|
1266
1342
|
|
1267
|
-
|
1268
|
-
on_enter { |event| ... }
|
1269
|
-
}
|
1343
|
+
on_enter { |event| ... }
|
1270
1344
|
end
|
1271
1345
|
```
|
1272
1346
|
|
1273
|
-
|
1347
|
+
You can easily create a more specific definition that adds new events and more specific callbacks to the mix.
|
1274
1348
|
|
1275
1349
|
```ruby
|
1276
1350
|
class SpecificStateMachine < GenericStateMachine
|
1277
|
-
|
1278
|
-
event :stop, :green => :yellow
|
1279
|
-
}
|
1351
|
+
event :stop, :green => :yellow
|
1280
1352
|
|
1281
|
-
|
1282
|
-
on_enter(:yellow) { |event| ... }
|
1283
|
-
}
|
1353
|
+
on_enter(:yellow) { |event| ... }
|
1284
1354
|
end
|
1285
1355
|
```
|
1286
1356
|
|
@@ -1288,16 +1358,15 @@ Finally to use the specific state machine definition do:
|
|
1288
1358
|
|
1289
1359
|
```ruby
|
1290
1360
|
specific_fsm = SpecificStateMachine.new
|
1291
|
-
specific_fsm.target ... # Target specific object
|
1292
1361
|
```
|
1293
1362
|
|
1294
|
-
##
|
1363
|
+
## 7. Integration
|
1295
1364
|
|
1296
|
-
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
|
1365
|
+
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 existing classes.
|
1297
1366
|
|
1298
|
-
###
|
1367
|
+
### 7.1 Plain Ruby Objects
|
1299
1368
|
|
1300
|
-
In order to use **FiniteMachine** with an object, you need to define a method that will construct the state machine. You can implement the state machine using the `
|
1369
|
+
In order to use **FiniteMachine** with an object, you need to define a method that will construct the state machine. You can implement the state machine using the `new` DSL or create a separate object that can be instantiated. To complete integration you will need to specify `target` context to allow state machine to communicate with the other methods inside the class like so:
|
1301
1370
|
|
1302
1371
|
```ruby
|
1303
1372
|
class Car
|
@@ -1314,32 +1383,25 @@ class Car
|
|
1314
1383
|
end
|
1315
1384
|
|
1316
1385
|
def gears
|
1317
|
-
|
1318
|
-
@gears ||= FiniteMachine.define do
|
1386
|
+
@gears ||= FiniteMachine.new(self) do
|
1319
1387
|
initial :neutral
|
1320
1388
|
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
event :shift, :one => :two
|
1326
|
-
event :shift, :two => :one
|
1327
|
-
event :back, [:neutral, :one] => :reverse
|
1328
|
-
}
|
1389
|
+
event :start, :neutral => :one
|
1390
|
+
event :shift, :one => :two
|
1391
|
+
event :shift, :two => :one
|
1392
|
+
event :back, [:neutral, :one] => :reverse
|
1329
1393
|
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
end
|
1394
|
+
on_enter :reverse do |event|
|
1395
|
+
target.turn_reverse_lights_on
|
1396
|
+
end
|
1334
1397
|
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1398
|
+
on_exit :reverse do |event|
|
1399
|
+
target.turn_reverse_lights_off
|
1400
|
+
end
|
1338
1401
|
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
}
|
1402
|
+
on_transition do |event|
|
1403
|
+
puts "shifted from #{event.from} to #{event.to}"
|
1404
|
+
end
|
1343
1405
|
end
|
1344
1406
|
end
|
1345
1407
|
end
|
@@ -1359,7 +1421,7 @@ car.gears.current # => :reverse
|
|
1359
1421
|
car.reverse_lights_on? # => true
|
1360
1422
|
```
|
1361
1423
|
|
1362
|
-
###
|
1424
|
+
### 7.2 ActiveRecord
|
1363
1425
|
|
1364
1426
|
In order to integrate **FiniteMachine** with ActiveRecord simply add a method with state machine definition. You can also define the state machine in separate module to aid reusability. Once the state machine is defined use the `target` helper to reference the current class. Having defined `target` you call ActiveRecord methods inside the callbacks to persist the state.
|
1365
1427
|
|
@@ -1383,22 +1445,15 @@ class Account < ActiveRecord::Base
|
|
1383
1445
|
end
|
1384
1446
|
|
1385
1447
|
def manage
|
1386
|
-
|
1387
|
-
@manage ||= FiniteMachine.define do
|
1388
|
-
target context
|
1389
|
-
|
1448
|
+
@manage ||= FiniteMachine.new(self) do
|
1390
1449
|
initial :unapproved
|
1391
1450
|
|
1392
|
-
|
1393
|
-
|
1394
|
-
event :authorize, :pending => :access
|
1395
|
-
}
|
1451
|
+
event :enqueue, :unapproved => :pending
|
1452
|
+
event :authorize, :pending => :access
|
1396
1453
|
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
end
|
1401
|
-
}
|
1454
|
+
on_enter do |event|
|
1455
|
+
target.state = event.to
|
1456
|
+
end
|
1402
1457
|
end
|
1403
1458
|
end
|
1404
1459
|
end
|
@@ -1411,11 +1466,11 @@ account.manage.authorize
|
|
1411
1466
|
account.state # => :access
|
1412
1467
|
```
|
1413
1468
|
|
1414
|
-
Please note that you do not need to call `target.save` inside callback, it is
|
1469
|
+
Please note that you do not need to call `target.save` inside callback, it is enough to just set the state. It is much more preferable to let the `ActiveRecord` object to persist when it makes sense for the application and thus keep the state machine focused on managing the state transitions.
|
1415
1470
|
|
1416
|
-
###
|
1471
|
+
### 7.3 Transactions
|
1417
1472
|
|
1418
|
-
When using **FiniteMachine** with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section
|
1473
|
+
When using **FiniteMachine** with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 7.2 one can run event in transaction in the following way:
|
1419
1474
|
|
1420
1475
|
```ruby
|
1421
1476
|
ActiveRecord::Base.transaction do
|
@@ -1427,7 +1482,7 @@ If the transition fails it will raise `TransitionError` which will cause the tra
|
|
1427
1482
|
|
1428
1483
|
Please check the ORM of your choice if it supports database transactions.
|
1429
1484
|
|
1430
|
-
##
|
1485
|
+
## 8 Tips
|
1431
1486
|
|
1432
1487
|
Creating a standalone **FiniteMachine** brings a number of 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.
|
1433
1488
|
|
@@ -1441,4 +1496,4 @@ Creating a standalone **FiniteMachine** brings a number of benefits, one of them
|
|
1441
1496
|
|
1442
1497
|
## Copyright
|
1443
1498
|
|
1444
|
-
Copyright (c) 2014
|
1499
|
+
Copyright (c) 2014 Piotr Murach. See LICENSE for further details.
|