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