simply_fsm 0.1.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile.lock +5 -1
- data/README.md +48 -16
- data/Rakefile +7 -0
- data/lib/simply_fsm/version.rb +1 -1
- data/lib/simply_fsm.rb +151 -33
- data/simply_fsm.gemspec +1 -0
- data/spec/unit/fail_events_spec.rb +3 -3
- data/spec/unit/guard_events_spec.rb +6 -6
- data/spec/unit/multi_transition_fail_events_spec.rb +64 -0
- data/spec/unit/multi_transition_state_machine_spec.rb +87 -0
- data/spec/unit/one_state_machine_spec.rb +3 -3
- data/spec/unit/two_state_machines_spec.rb +4 -4
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ae5d7b10e3ba09f4838625a6de8dd2314b68246628efed8d950fe087f34f1d7
|
|
4
|
+
data.tar.gz: cb2f1af32ed62f1032e902391543e6347d0c367eabbe1795e0caf067099baece
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f8e0821322ec9e24b9eb5626d59c30e8fa5863d174c53c1d77d041351a635f0a690a111057c3bbd8d58e2aa8418384996314fb39e1f43db3c31c825f229045b
|
|
7
|
+
data.tar.gz: f7fc248239ca69a84f54ac8a94e25e6bcfc7986e37c40c4bd2dbc5936d4e2a4168be5afbef66351c94f7aabc07debc27ab78ff8133426a6d140082a19f2f2138
|
data/.gitignore
CHANGED
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--no-private
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
- None right now
|
|
4
4
|
|
|
5
|
+
## [0.2.3] - 2022-04-09
|
|
6
|
+
|
|
7
|
+
- Add `rake yard` to generate local documentation
|
|
8
|
+
- Clean up API documentation
|
|
9
|
+
- Privatise some internal methods
|
|
10
|
+
|
|
11
|
+
## [0.2.2] - 2022-03-08
|
|
12
|
+
|
|
13
|
+
- Call `fail` lambda without wrapping it in a lambda
|
|
14
|
+
|
|
15
|
+
## [0.2.1] - 2022-03-05
|
|
16
|
+
|
|
17
|
+
- Fixed bug where named fail handlers were not called properly for multi-transition events.
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2022-03-01
|
|
20
|
+
|
|
21
|
+
- *Breaks API* (sorry!)
|
|
22
|
+
- When declaring events in a state machine, use `transitions:` (as in "transitions from X to Y") instead of `transition:`
|
|
23
|
+
- Added support for multiple transitions per event
|
|
24
|
+
|
|
5
25
|
## [0.1.2] - 2022-02-28
|
|
6
26
|
|
|
7
27
|
- Cleaned up source with smaller clearer methods
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
simply_fsm (0.
|
|
4
|
+
simply_fsm (0.2.3)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -46,6 +46,9 @@ GEM
|
|
|
46
46
|
ruby-progressbar (1.11.0)
|
|
47
47
|
stringio (3.0.1)
|
|
48
48
|
unicode-display_width (2.1.0)
|
|
49
|
+
webrick (1.7.0)
|
|
50
|
+
yard (0.9.27)
|
|
51
|
+
webrick (~> 1.7.0)
|
|
49
52
|
|
|
50
53
|
PLATFORMS
|
|
51
54
|
ruby
|
|
@@ -56,6 +59,7 @@ DEPENDENCIES
|
|
|
56
59
|
rspec (~> 3.0)
|
|
57
60
|
rubocop (~> 1.21)
|
|
58
61
|
simply_fsm!
|
|
62
|
+
yard
|
|
59
63
|
|
|
60
64
|
BUNDLED WITH
|
|
61
65
|
2.1.4
|
data/README.md
CHANGED
|
@@ -13,6 +13,7 @@ If you need storage/persistence/Rails/etc support, I recommend [AASM](https://gi
|
|
|
13
13
|
- [Multiple state machines](#multiple-state-machines)
|
|
14
14
|
- [Handle failed events](#handle-failed-events)
|
|
15
15
|
- [Guarding events](#guarding-events)
|
|
16
|
+
- [Multiple transitions for an event](#multiple-transitions-for-an-event)
|
|
16
17
|
- [Development](#development)
|
|
17
18
|
- [Contributing](#contributing)
|
|
18
19
|
- [License](#license)
|
|
@@ -49,15 +50,15 @@ class Job
|
|
|
49
50
|
state :running
|
|
50
51
|
state :cleaning
|
|
51
52
|
|
|
52
|
-
event :run,
|
|
53
|
+
event :run, transitions: { from: :sleeping, to: :running } do
|
|
53
54
|
# executed when transition succeeds
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
event :clean,
|
|
57
|
+
event :clean, transitions: { from: :running, to: :cleaning } do
|
|
57
58
|
# do the cleaning since transition succeeded
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
event :sleep,
|
|
61
|
+
event :sleep, transitions: { from: [:running, :cleaning], to: :sleeping }
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
```
|
|
@@ -88,16 +89,16 @@ class Player
|
|
|
88
89
|
state :idling, initial: true
|
|
89
90
|
state :walking
|
|
90
91
|
|
|
91
|
-
event :idle,
|
|
92
|
-
event :walk,
|
|
92
|
+
event :idle, transitions: { from: :any, to: :idling }
|
|
93
|
+
event :walk, transitions: { from: :idling, to: :walking }
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
state_machine :action do
|
|
96
97
|
state :ready, initial: true
|
|
97
98
|
state :blocking
|
|
98
99
|
|
|
99
|
-
event :hold,
|
|
100
|
-
event :block,
|
|
100
|
+
event :hold, transitions: { from: :any, to: :ready }
|
|
101
|
+
event :block, transitions: { from: :any, to: :blocking }
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
```
|
|
@@ -126,11 +127,11 @@ class JobWithErrors
|
|
|
126
127
|
state :running
|
|
127
128
|
state :cleaning
|
|
128
129
|
|
|
129
|
-
event :sleep,
|
|
130
|
-
event :clean,
|
|
130
|
+
event :sleep, transitions: { from: %i[running cleaning], to: :sleeping }
|
|
131
|
+
event :clean, transitions: { from: :running, to: :cleaning }
|
|
131
132
|
event :run,
|
|
132
133
|
fail: ->(_event) { raise RunError, "Cannot run" },
|
|
133
|
-
|
|
134
|
+
transitions: { from: :sleeping, to: :running }
|
|
134
135
|
end
|
|
135
136
|
|
|
136
137
|
def on_any_fail(event_name)
|
|
@@ -154,9 +155,9 @@ class AgilePlayer
|
|
|
154
155
|
state :walking
|
|
155
156
|
state :running
|
|
156
157
|
|
|
157
|
-
event :idle,
|
|
158
|
-
event :walk,
|
|
159
|
-
event :run,
|
|
158
|
+
event :idle, transitions: { from: :any, to: :idling }
|
|
159
|
+
event :walk, transitions: { from: :any, to: :walking }
|
|
160
|
+
event :run, transitions: { from: :any, to: :running }
|
|
160
161
|
end
|
|
161
162
|
|
|
162
163
|
state_machine :action do
|
|
@@ -164,14 +165,45 @@ class AgilePlayer
|
|
|
164
165
|
state :jumping
|
|
165
166
|
state :leaping
|
|
166
167
|
|
|
167
|
-
event :hold,
|
|
168
|
+
event :hold, transitions: { from: :any, to: :ready }
|
|
168
169
|
event :jump,
|
|
169
170
|
guard: -> { !running? },
|
|
170
|
-
|
|
171
|
+
transitions: { from: :ready, to: :jumping }
|
|
171
172
|
event :leap,
|
|
172
173
|
guard: -> { running? },
|
|
173
174
|
fail: ->(_event) { raise LeapError, "Cannot leap" },
|
|
174
|
-
|
|
175
|
+
transitions: { from: :ready, to: :leaping }
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
### Multiple transitions for an event
|
|
180
|
+
|
|
181
|
+
Sometimes a single event can transition to different end states based on different input states. In those situations you can specify an array of transitions. Consider the following example where the `hunt` event transitions to `walking` or `running` depending on some condition outside the state machine.
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class Critter
|
|
185
|
+
include SimplyFSM
|
|
186
|
+
|
|
187
|
+
def tired?
|
|
188
|
+
@ate_at <= 12.hours.ago || @slept_at <= 24.hours.ago
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
state_machine :activity do
|
|
192
|
+
state :sleeping, initial: true
|
|
193
|
+
state :running
|
|
194
|
+
state :walking
|
|
195
|
+
state :eating
|
|
196
|
+
|
|
197
|
+
event :eat, transitions: { to: :eating } do
|
|
198
|
+
@ate_at = DateTime.new
|
|
199
|
+
end
|
|
200
|
+
event :sleep, transitions: { from: :eating, to: :sleeping } do
|
|
201
|
+
@slept_at = DateTime.new
|
|
202
|
+
end
|
|
203
|
+
event :hunt, transitions: [
|
|
204
|
+
{ when: -> { tired? }, to: :walking },
|
|
205
|
+
{ to: :running }
|
|
206
|
+
]
|
|
175
207
|
end
|
|
176
208
|
end
|
|
177
209
|
```
|
data/Rakefile
CHANGED
|
@@ -17,6 +17,13 @@ Rake::RDocTask.new do |rdoc|
|
|
|
17
17
|
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
require "yard"
|
|
21
|
+
|
|
22
|
+
YARD::Rake::YardocTask.new do |t|
|
|
23
|
+
t.files = ["lib/**/*.rb"]
|
|
24
|
+
t.stats_options = ["--list-undoc"]
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
RuboCop::RakeTask.new
|
|
21
28
|
|
|
22
29
|
task default: %i[spec rubocop]
|
data/lib/simply_fsm/version.rb
CHANGED
data/lib/simply_fsm.rb
CHANGED
|
@@ -2,20 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require "simply_fsm/version"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
#
|
|
5
|
+
#
|
|
6
|
+
# Include *SimplyFSM* in a class to be able to defined state machines.
|
|
7
|
+
#
|
|
7
8
|
module SimplyFSM
|
|
9
|
+
#
|
|
10
|
+
# Provides a +state_machine+ for the including class.
|
|
8
11
|
def self.included(base)
|
|
9
12
|
base.extend(ClassMethods)
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
#
|
|
13
16
|
# Defines the constructor for defining a state machine
|
|
14
17
|
module ClassMethods
|
|
15
|
-
|
|
18
|
+
#
|
|
16
19
|
# Declare a state machine called +name+ which can then be defined
|
|
17
|
-
# by a DSL defined by the methods of
|
|
18
|
-
#
|
|
20
|
+
# by a DSL defined by the methods of *StateMachine*.
|
|
21
|
+
#
|
|
22
|
+
# @param [String] name of the state machine.
|
|
23
|
+
# @param [Hash] opts to specify options such as:
|
|
24
|
+
# - +fail+ lambda that is called with the event name when any event fails to transition
|
|
25
|
+
#
|
|
19
26
|
def state_machine(name, opts = {}, &block)
|
|
20
27
|
fsm = StateMachine.new(name, self, fail: opts[:fail])
|
|
21
28
|
fsm.instance_eval(&block)
|
|
@@ -23,10 +30,19 @@ module SimplyFSM
|
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
##
|
|
26
|
-
# The DSL for defining a state machine
|
|
33
|
+
# The DSL for defining a state machine. These methods are used within the declaration of a +state_machine+.
|
|
34
|
+
#
|
|
35
|
+
# @attr_reader [String] initial_state The initial state of the state machine
|
|
36
|
+
# @attr_reader [Array] states All the states of the state machine
|
|
37
|
+
# @attr_reader [Array] events All the events of the state machine
|
|
38
|
+
# @attr_reader [String] name
|
|
39
|
+
# @attr_reader [String] full_name The name of the owning class combined with the state machine's name
|
|
40
|
+
#
|
|
27
41
|
class StateMachine
|
|
28
42
|
attr_reader :initial_state, :states, :events, :name, :full_name
|
|
29
43
|
|
|
44
|
+
#
|
|
45
|
+
# @!visibility private
|
|
30
46
|
def initialize(name, owner_class, fail: nil)
|
|
31
47
|
@owner_class = owner_class
|
|
32
48
|
@name = name.to_sym
|
|
@@ -39,8 +55,12 @@ module SimplyFSM
|
|
|
39
55
|
setup_base_methods
|
|
40
56
|
end
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
#
|
|
43
59
|
# Declare a supported +state_name+, and optionally specify one as the +initial+ state.
|
|
60
|
+
#
|
|
61
|
+
# @param [String] state_name
|
|
62
|
+
# @param [Boolean] initial to indicate if this is the initial state of the state machine
|
|
63
|
+
#
|
|
44
64
|
def state(state_name, initial: false)
|
|
45
65
|
return if state_name.nil? || @states.include?(state_name)
|
|
46
66
|
|
|
@@ -55,19 +75,30 @@ module SimplyFSM
|
|
|
55
75
|
end
|
|
56
76
|
|
|
57
77
|
##
|
|
58
|
-
# Define an event by +event_name+
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
# Define an event by +event_name+
|
|
79
|
+
#
|
|
80
|
+
# @param [String] event_name
|
|
81
|
+
# @param [Hash,Array] transitions either one (Hash) or many (Array of Hashes) transitions +from+ one state +to+ another state.
|
|
82
|
+
# @param [Lambda] guard if specified must return +true+ before any transitions are attempted
|
|
83
|
+
# @param [Lambda] fail called with event name if specified when all the attempted transitions fail
|
|
84
|
+
# @yield when the transition attempt succeeds.
|
|
85
|
+
def event(event_name, transitions:, guard: nil, fail: nil, &after)
|
|
86
|
+
return unless event_exists?(event_name) && transitions
|
|
65
87
|
|
|
66
88
|
@events << event_name
|
|
67
|
-
to = transition[:to]
|
|
68
89
|
may_event_name = "may_#{event_name}?"
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
if transitions.is_a?(Array)
|
|
92
|
+
setup_multi_transition_may_event_method transitions: transitions, guard: guard,
|
|
93
|
+
may_event_name: may_event_name
|
|
94
|
+
setup_multi_transition_event_method event_name,
|
|
95
|
+
transitions: transitions, guard: guard,
|
|
96
|
+
var_name: "@#{@name}", fail: fail || @fail_handler
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
to = transitions[:to]
|
|
101
|
+
setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], guard
|
|
71
102
|
setup_event_method event_name, var_name: "@#{@name}",
|
|
72
103
|
may_event_name: may_event_name, to: to,
|
|
73
104
|
fail: fail || @fail_handler, &after
|
|
@@ -75,47 +106,107 @@ module SimplyFSM
|
|
|
75
106
|
|
|
76
107
|
private
|
|
77
108
|
|
|
109
|
+
def setup_multi_transition_may_event_method(transitions:, guard:, may_event_name:)
|
|
110
|
+
state_machine_name = @name
|
|
111
|
+
|
|
112
|
+
make_owner_method may_event_name, lambda {
|
|
113
|
+
if !guard || instance_exec(&guard)
|
|
114
|
+
current = send(state_machine_name)
|
|
115
|
+
# Check each transition, and first one that succeeds ends the scan
|
|
116
|
+
transitions.each do |t|
|
|
117
|
+
next if cannot_transition?(t[:from], t[:when], current)
|
|
118
|
+
|
|
119
|
+
return true
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
false
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def setup_fail_lambda_for(fail)
|
|
127
|
+
return unless fail
|
|
128
|
+
|
|
129
|
+
if fail.is_a?(String) || fail.is_a?(Symbol)
|
|
130
|
+
->(event_name) { send(fail, event_name) }
|
|
131
|
+
else
|
|
132
|
+
fail
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
|
|
137
|
+
state_machine_name = @name
|
|
138
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
139
|
+
make_owner_method event_name, lambda {
|
|
140
|
+
if !guard || instance_exec(&guard)
|
|
141
|
+
current = send(state_machine_name)
|
|
142
|
+
# Check each transition, and first one that succeeds ends the scan
|
|
143
|
+
transitions.each do |t|
|
|
144
|
+
next if cannot_transition?(t[:from], t[:when], current)
|
|
145
|
+
|
|
146
|
+
instance_variable_set(var_name, t[:to])
|
|
147
|
+
return true
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
151
|
+
false
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
78
155
|
def event_exists?(event_name)
|
|
79
156
|
event_name && !@events.include?(event_name)
|
|
80
157
|
end
|
|
81
158
|
|
|
82
159
|
def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
|
|
160
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
83
161
|
method_lambda = lambda {
|
|
84
162
|
if send(may_event_name)
|
|
85
163
|
instance_variable_set(var_name, to)
|
|
86
164
|
instance_exec(&after) if after
|
|
87
165
|
return true
|
|
88
166
|
end
|
|
89
|
-
|
|
90
|
-
if fail
|
|
91
|
-
if fail.is_a?(String) || fail.is_a?(Symbol)
|
|
92
|
-
send(fail, event_name)
|
|
93
|
-
else
|
|
94
|
-
instance_exec(event_name, &fail)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
167
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
97
168
|
false
|
|
98
169
|
}
|
|
99
170
|
make_owner_method event_name, method_lambda
|
|
100
171
|
end
|
|
101
172
|
|
|
102
|
-
def setup_may_event_method(may_event_name, from,
|
|
173
|
+
def setup_may_event_method(may_event_name, from, cond, guard)
|
|
103
174
|
state_machine_name = @name
|
|
104
175
|
#
|
|
105
176
|
# Instead of one "may_event?" method that checks all variations every time it's called, here we check
|
|
106
177
|
# the event definition and define the most optimal lambda to ensure the check is as fast as possible
|
|
107
|
-
method_lambda = if from == :any
|
|
108
|
-
|
|
109
|
-
elsif from == :any
|
|
110
|
-
guard # guarded transition from any state
|
|
111
|
-
elsif !guard
|
|
112
|
-
guardless_may_event_lambda(from, state_machine_name)
|
|
178
|
+
method_lambda = if from == :any
|
|
179
|
+
from_any_may_event_lambda(guard, cond, state_machine_name)
|
|
113
180
|
else
|
|
114
|
-
|
|
181
|
+
guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
115
182
|
end
|
|
116
183
|
make_owner_method may_event_name, method_lambda
|
|
117
184
|
end
|
|
118
185
|
|
|
186
|
+
def from_any_may_event_lambda(guard, cond, _state_machine_name)
|
|
187
|
+
if !guard && !cond
|
|
188
|
+
-> { true } # unguarded transition from any state
|
|
189
|
+
elsif !cond
|
|
190
|
+
guard # guarded transition from any state
|
|
191
|
+
elsif !guard
|
|
192
|
+
cond # conditional unguarded transition from any state
|
|
193
|
+
else
|
|
194
|
+
-> { instance_exec(&guard) && instance_exec(&cond) }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
199
|
+
if !guard && !cond
|
|
200
|
+
guardless_may_event_lambda(from, state_machine_name)
|
|
201
|
+
elsif !cond
|
|
202
|
+
guarded_may_event_lambda(from, guard, state_machine_name)
|
|
203
|
+
elsif !guard
|
|
204
|
+
guarded_may_event_lambda(from, cond, state_machine_name)
|
|
205
|
+
else
|
|
206
|
+
guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
119
210
|
def guarded_may_event_lambda(from, guard, state_machine_name)
|
|
120
211
|
if from.is_a?(Array)
|
|
121
212
|
lambda { # guarded transition from choice of states
|
|
@@ -130,6 +221,20 @@ module SimplyFSM
|
|
|
130
221
|
end
|
|
131
222
|
end
|
|
132
223
|
|
|
224
|
+
def guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
225
|
+
if from.is_a?(Array)
|
|
226
|
+
lambda { # guarded transition from choice of states
|
|
227
|
+
current = send(state_machine_name)
|
|
228
|
+
from.include?(current) && instance_exec(&guard) && instance_exec(&cond)
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
lambda { # guarded transition from one state
|
|
232
|
+
current = send(state_machine_name)
|
|
233
|
+
from == current && instance_exec(&guard) && instance_exec(&cond)
|
|
234
|
+
}
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
133
238
|
def guardless_may_event_lambda(from, state_machine_name)
|
|
134
239
|
if from.is_a?(Array)
|
|
135
240
|
lambda { # unguarded transition from choice of states
|
|
@@ -159,4 +264,17 @@ module SimplyFSM
|
|
|
159
264
|
@owner_class.define_method(method_name, method_definition)
|
|
160
265
|
end
|
|
161
266
|
end
|
|
267
|
+
|
|
268
|
+
private
|
|
269
|
+
|
|
270
|
+
def state_match?(from, current)
|
|
271
|
+
return true if from == :any
|
|
272
|
+
return from.include?(current) if from.is_a?(Array)
|
|
273
|
+
|
|
274
|
+
from == current
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def cannot_transition?(from, cond, current)
|
|
278
|
+
(from && !state_match?(from, current)) || (cond && !instance_exec(&cond))
|
|
279
|
+
end
|
|
162
280
|
end
|
data/simply_fsm.gemspec
CHANGED
|
@@ -11,11 +11,11 @@ class FailHandlingStateMachine
|
|
|
11
11
|
state :running
|
|
12
12
|
state :cleaning
|
|
13
13
|
|
|
14
|
-
event :sleep,
|
|
15
|
-
event :clean,
|
|
14
|
+
event :sleep, transitions: { from: %i[running cleaning], to: :sleeping }
|
|
15
|
+
event :clean, transitions: { from: :running, to: :cleaning }
|
|
16
16
|
event :run,
|
|
17
17
|
fail: ->(_event) { raise RunError, "Cannot run" },
|
|
18
|
-
|
|
18
|
+
transitions: { from: :sleeping, to: :running }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def on_any_fail(event_name)
|
|
@@ -10,9 +10,9 @@ class GuardingEvents
|
|
|
10
10
|
state :walking
|
|
11
11
|
state :running
|
|
12
12
|
|
|
13
|
-
event :idle,
|
|
14
|
-
event :walk,
|
|
15
|
-
event :run,
|
|
13
|
+
event :idle, transitions: { to: :idling }
|
|
14
|
+
event :walk, transitions: { from: :any, to: :walking }
|
|
15
|
+
event :run, transitions: { from: :any, to: :running }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
state_machine :action do
|
|
@@ -20,14 +20,14 @@ class GuardingEvents
|
|
|
20
20
|
state :jumping
|
|
21
21
|
state :leaping
|
|
22
22
|
|
|
23
|
-
event :hold,
|
|
23
|
+
event :hold, transitions: { to: :ready }
|
|
24
24
|
event :jump,
|
|
25
25
|
guard: -> { !running? },
|
|
26
|
-
|
|
26
|
+
transitions: { from: :ready, to: :jumping }
|
|
27
27
|
event :leap,
|
|
28
28
|
guard: -> { running? },
|
|
29
29
|
fail: ->(_event) { raise LeapError, "Cannot leap" },
|
|
30
|
-
|
|
30
|
+
transitions: { when: -> { ready? }, to: :leaping }
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MultiTransitionFailHandlingStateMachine
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class RunError < StandardError; end
|
|
6
|
+
|
|
7
|
+
include SimplyFSM
|
|
8
|
+
|
|
9
|
+
state_machine :activity, fail: :on_any_fail do
|
|
10
|
+
state :sleeping, initial: true
|
|
11
|
+
state :running
|
|
12
|
+
state :cleaning
|
|
13
|
+
|
|
14
|
+
event :run,
|
|
15
|
+
fail: ->(_event) { raise RunError, "Cannot run" },
|
|
16
|
+
transitions: [{ from: :sleeping, to: :running }]
|
|
17
|
+
|
|
18
|
+
event :clean, transitions: [
|
|
19
|
+
{ from: :running, to: :cleaning }
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
event :sleep, transitions: [
|
|
23
|
+
{ from: :running, to: :sleeping },
|
|
24
|
+
{ when: -> { cleaning? }, to: :sleeping }
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_any_fail(event_name)
|
|
29
|
+
raise Error, "Cannot do: #{event_name}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
RSpec.describe MultiTransitionFailHandlingStateMachine do
|
|
34
|
+
describe "#sleep" do
|
|
35
|
+
it "error if already sleeping" do
|
|
36
|
+
expect { subject.sleep }.to raise_error(MultiTransitionFailHandlingStateMachine::Error)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "#run" do
|
|
41
|
+
it "custom error if already running" do
|
|
42
|
+
subject.run
|
|
43
|
+
expect { subject.run }.to raise_error(MultiTransitionFailHandlingStateMachine::RunError)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "custom error if cleaning" do
|
|
47
|
+
subject.run
|
|
48
|
+
subject.clean
|
|
49
|
+
expect { subject.run }.to raise_error(MultiTransitionFailHandlingStateMachine::RunError)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "#clean" do
|
|
54
|
+
it "error if sleeping" do
|
|
55
|
+
expect { subject.clean }.to raise_error(MultiTransitionFailHandlingStateMachine::Error)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "error if already cleaning" do
|
|
59
|
+
subject.run
|
|
60
|
+
subject.clean
|
|
61
|
+
expect { subject.clean }.to raise_error(MultiTransitionFailHandlingStateMachine::Error)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class MultiTransitionStateMachine
|
|
4
|
+
include SimplyFSM
|
|
5
|
+
|
|
6
|
+
state_machine :activity do
|
|
7
|
+
state :sleeping, initial: true
|
|
8
|
+
state :running
|
|
9
|
+
state :cleaning
|
|
10
|
+
|
|
11
|
+
event :run, transitions: { from: :sleeping, to: :running }
|
|
12
|
+
|
|
13
|
+
event :clean, transitions: [
|
|
14
|
+
{ from: :running, to: :cleaning }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
event :sleep, transitions: [
|
|
18
|
+
{ from: :running, to: :sleeping },
|
|
19
|
+
{ when: -> { cleaning? }, to: :sleeping }
|
|
20
|
+
]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
RSpec.describe MultiTransitionStateMachine do
|
|
25
|
+
include_examples "state machine basics", :activity,
|
|
26
|
+
initial_state: :sleeping,
|
|
27
|
+
states: %i[sleeping running cleaning],
|
|
28
|
+
events: %i[run clean sleep]
|
|
29
|
+
|
|
30
|
+
describe "#sleep" do
|
|
31
|
+
it "fails if already sleeping" do
|
|
32
|
+
expect(subject.may_sleep?).to be false
|
|
33
|
+
expect(subject.sleep).to be false
|
|
34
|
+
end
|
|
35
|
+
it "succeeds if running" do
|
|
36
|
+
subject.run
|
|
37
|
+
expect(subject.may_sleep?).to be true
|
|
38
|
+
expect(subject.sleep).to be true
|
|
39
|
+
end
|
|
40
|
+
it "succeeds if cleaning" do
|
|
41
|
+
subject.run
|
|
42
|
+
subject.clean
|
|
43
|
+
expect(subject.may_sleep?).to be true
|
|
44
|
+
expect(subject.sleep).to be true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "#run" do
|
|
49
|
+
it "succeeds if sleeping" do
|
|
50
|
+
expect(subject.may_run?).to be true
|
|
51
|
+
expect(subject.run).to be true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "fails if already running" do
|
|
55
|
+
subject.run
|
|
56
|
+
expect(subject.may_run?).to be false
|
|
57
|
+
expect(subject.run).to be false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "fails if cleaning" do
|
|
61
|
+
subject.run
|
|
62
|
+
subject.clean
|
|
63
|
+
expect(subject.may_run?).to be false
|
|
64
|
+
expect(subject.run).to be false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "#clean" do
|
|
69
|
+
it "succeeds if running" do
|
|
70
|
+
subject.run
|
|
71
|
+
expect(subject.may_clean?).to be true
|
|
72
|
+
expect(subject.clean).to be true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "fails if sleeping" do
|
|
76
|
+
expect(subject.may_clean?).to be false
|
|
77
|
+
expect(subject.clean).to be false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "fails if already cleaning" do
|
|
81
|
+
subject.run
|
|
82
|
+
subject.clean
|
|
83
|
+
expect(subject.may_clean?).to be false
|
|
84
|
+
expect(subject.clean).to be false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -8,9 +8,9 @@ class OneStateMachine
|
|
|
8
8
|
state :running
|
|
9
9
|
state :cleaning
|
|
10
10
|
|
|
11
|
-
event :run,
|
|
12
|
-
event :clean,
|
|
13
|
-
event :sleep,
|
|
11
|
+
event :run, transitions: { from: :sleeping, to: :running }
|
|
12
|
+
event :clean, transitions: { from: :running, to: :cleaning }
|
|
13
|
+
event :sleep, transitions: { from: %i[running cleaning], to: :sleeping }
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -7,16 +7,16 @@ class TwoStateMachines
|
|
|
7
7
|
state :idling, initial: true
|
|
8
8
|
state :walking
|
|
9
9
|
|
|
10
|
-
event :idle,
|
|
11
|
-
event :walk,
|
|
10
|
+
event :idle, transitions: { from: :any, to: :idling }
|
|
11
|
+
event :walk, transitions: { from: :idling, to: :walking }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
state_machine :action do
|
|
15
15
|
state :ready, initial: true
|
|
16
16
|
state :blocking
|
|
17
17
|
|
|
18
|
-
event :hold,
|
|
19
|
-
event :block,
|
|
18
|
+
event :hold, transitions: { from: :any, to: :ready }
|
|
19
|
+
event :block, transitions: { from: :any, to: :blocking }
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simply_fsm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nogginly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-04-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rdoc
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: yard
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rspec
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -49,6 +63,7 @@ files:
|
|
|
49
63
|
- ".rspec"
|
|
50
64
|
- ".rubocop.yml"
|
|
51
65
|
- ".ruby-version"
|
|
66
|
+
- ".yardopts"
|
|
52
67
|
- CHANGELOG.md
|
|
53
68
|
- CODE_OF_CONDUCT.md
|
|
54
69
|
- Gemfile
|
|
@@ -65,6 +80,8 @@ files:
|
|
|
65
80
|
- spec/support/state_machine_examples.rb
|
|
66
81
|
- spec/unit/fail_events_spec.rb
|
|
67
82
|
- spec/unit/guard_events_spec.rb
|
|
83
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
84
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
68
85
|
- spec/unit/one_state_machine_spec.rb
|
|
69
86
|
- spec/unit/simply_fsm_spec.rb
|
|
70
87
|
- spec/unit/two_state_machines_spec.rb
|
|
@@ -99,6 +116,8 @@ test_files:
|
|
|
99
116
|
- spec/support/state_machine_examples.rb
|
|
100
117
|
- spec/unit/fail_events_spec.rb
|
|
101
118
|
- spec/unit/guard_events_spec.rb
|
|
119
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
120
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
102
121
|
- spec/unit/one_state_machine_spec.rb
|
|
103
122
|
- spec/unit/simply_fsm_spec.rb
|
|
104
123
|
- spec/unit/two_state_machines_spec.rb
|