simply_fsm 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rspec +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +5 -1
- data/Rakefile +7 -0
- data/lib/simply_fsm/simply_fsm.rb +278 -0
- data/lib/simply_fsm/version.rb +1 -1
- data/lib/simply_fsm.rb +1 -249
- data/simply_fsm.gemspec +1 -0
- data/spec/unit/multi_transition_fail_events_spec.rb +64 -0
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e96963bd4f2d0cf605ca52b79cc74c3b3ba29d66df452683c6425f6e4243cf62
|
|
4
|
+
data.tar.gz: ca0ad99246f5f27eb9749ba2dd41c652bd424c55828cdeb4f6365478d8866488
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37de0fd3446033ef390fb8aa6bf616c936de2125e65b876724904b989163d816f52971198a5fb2055a9412589565633a5fbaaa93eaed35912da549bc366d1446
|
|
7
|
+
data.tar.gz: 97165581f0a8f1955ffbb568d99958e84a99de955041ead58b52a5ed100eee52fd8cc58175f32702a11c0d4d9c86a62ff858cc306b3b3a0efd52ec5e3cfdcf02
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--no-private
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
- None right now
|
|
4
4
|
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
- Moving main source under the `lib/simply_fsm` folder to remove `require` statement
|
|
8
|
+
- Why? Eventually want to be able to use this with `mruby`
|
|
9
|
+
|
|
10
|
+
## [0.2.3] - 2022-04-09
|
|
11
|
+
|
|
12
|
+
- Add `rake yard` to generate local documentation
|
|
13
|
+
- Clean up API documentation
|
|
14
|
+
- Privatise some internal methods
|
|
15
|
+
|
|
16
|
+
## [0.2.2] - 2022-03-08
|
|
17
|
+
|
|
18
|
+
- Call `fail` lambda without wrapping it in a lambda
|
|
19
|
+
|
|
20
|
+
## [0.2.1] - 2022-03-05
|
|
21
|
+
|
|
22
|
+
- Fixed bug where named fail handlers were not called properly for multi-transition events.
|
|
23
|
+
|
|
5
24
|
## [0.2.0] - 2022-03-01
|
|
6
25
|
|
|
7
26
|
- *Breaks API* (sorry!)
|
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.3.0)
|
|
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/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]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Include *SimplyFSM* in a class to be able to defined state machines.
|
|
5
|
+
#
|
|
6
|
+
module SimplyFSM
|
|
7
|
+
#
|
|
8
|
+
# Provides a +state_machine+ for the including class.
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# Defines the constructor for defining a state machine
|
|
15
|
+
module ClassMethods
|
|
16
|
+
#
|
|
17
|
+
# Declare a state machine called +name+ which can then be defined
|
|
18
|
+
# by a DSL defined by the methods of *StateMachine*.
|
|
19
|
+
#
|
|
20
|
+
# @param [String] name of the state machine.
|
|
21
|
+
# @param [Hash] opts to specify options such as:
|
|
22
|
+
# - +fail+ lambda that is called with the event name when any event fails to transition
|
|
23
|
+
#
|
|
24
|
+
def state_machine(name, opts = {}, &block)
|
|
25
|
+
fsm = StateMachine.new(name, self, fail: opts[:fail])
|
|
26
|
+
fsm.instance_eval(&block)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# The DSL for defining a state machine. These methods are used within the declaration of a +state_machine+.
|
|
32
|
+
#
|
|
33
|
+
# @attr_reader [String] initial_state The initial state of the state machine
|
|
34
|
+
# @attr_reader [Array] states All the states of the state machine
|
|
35
|
+
# @attr_reader [Array] events All the events of the state machine
|
|
36
|
+
# @attr_reader [String] name
|
|
37
|
+
# @attr_reader [String] full_name The name of the owning class combined with the state machine's name
|
|
38
|
+
#
|
|
39
|
+
class StateMachine
|
|
40
|
+
attr_reader :initial_state, :states, :events, :name, :full_name
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# @!visibility private
|
|
44
|
+
def initialize(name, owner_class, fail: nil)
|
|
45
|
+
@owner_class = owner_class
|
|
46
|
+
@name = name.to_sym
|
|
47
|
+
@full_name = "#{owner_class.name}/#{name}"
|
|
48
|
+
@states = []
|
|
49
|
+
@events = []
|
|
50
|
+
@initial_state = nil
|
|
51
|
+
@fail_handler = fail
|
|
52
|
+
|
|
53
|
+
setup_base_methods
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
# Declare a supported +state_name+, and optionally specify one as the +initial+ state.
|
|
58
|
+
#
|
|
59
|
+
# @param [String] state_name
|
|
60
|
+
# @param [Boolean] initial to indicate if this is the initial state of the state machine
|
|
61
|
+
#
|
|
62
|
+
def state(state_name, initial: false)
|
|
63
|
+
return if state_name.nil? || @states.include?(state_name)
|
|
64
|
+
|
|
65
|
+
status = state_name.to_sym
|
|
66
|
+
state_machine_name = @name
|
|
67
|
+
@states << status
|
|
68
|
+
@initial_state = status if initial
|
|
69
|
+
|
|
70
|
+
make_owner_method "#{state_name}?", lambda {
|
|
71
|
+
send(state_machine_name) == status
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Define an event by +event_name+
|
|
77
|
+
#
|
|
78
|
+
# @param [String] event_name
|
|
79
|
+
# @param [Hash,Array] transitions either one (Hash) or many (Array of Hashes) transitions +from+ one state +to+ another state.
|
|
80
|
+
# @param [Lambda] guard if specified must return +true+ before any transitions are attempted
|
|
81
|
+
# @param [Lambda] fail called with event name if specified when all the attempted transitions fail
|
|
82
|
+
# @yield when the transition attempt succeeds.
|
|
83
|
+
def event(event_name, transitions:, guard: nil, fail: nil, &after)
|
|
84
|
+
return unless event_exists?(event_name) && transitions
|
|
85
|
+
|
|
86
|
+
@events << event_name
|
|
87
|
+
may_event_name = "may_#{event_name}?"
|
|
88
|
+
|
|
89
|
+
if transitions.is_a?(Array)
|
|
90
|
+
setup_multi_transition_may_event_method transitions: transitions, guard: guard,
|
|
91
|
+
may_event_name: may_event_name
|
|
92
|
+
setup_multi_transition_event_method event_name,
|
|
93
|
+
transitions: transitions, guard: guard,
|
|
94
|
+
var_name: "@#{@name}", fail: fail || @fail_handler
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
to = transitions[:to]
|
|
99
|
+
setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], guard
|
|
100
|
+
setup_event_method event_name, var_name: "@#{@name}",
|
|
101
|
+
may_event_name: may_event_name, to: to,
|
|
102
|
+
fail: fail || @fail_handler, &after
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def setup_multi_transition_may_event_method(transitions:, guard:, may_event_name:)
|
|
108
|
+
state_machine_name = @name
|
|
109
|
+
|
|
110
|
+
make_owner_method may_event_name, lambda {
|
|
111
|
+
if !guard || instance_exec(&guard)
|
|
112
|
+
current = send(state_machine_name)
|
|
113
|
+
# Check each transition, and first one that succeeds ends the scan
|
|
114
|
+
transitions.each do |t|
|
|
115
|
+
next if cannot_transition?(t[:from], t[:when], current)
|
|
116
|
+
|
|
117
|
+
return true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
false
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def setup_fail_lambda_for(fail)
|
|
125
|
+
return unless fail
|
|
126
|
+
|
|
127
|
+
if fail.is_a?(String) || fail.is_a?(Symbol)
|
|
128
|
+
->(event_name) { send(fail, event_name) }
|
|
129
|
+
else
|
|
130
|
+
fail
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
|
|
135
|
+
state_machine_name = @name
|
|
136
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
137
|
+
make_owner_method event_name, lambda {
|
|
138
|
+
if !guard || instance_exec(&guard)
|
|
139
|
+
current = send(state_machine_name)
|
|
140
|
+
# Check each transition, and first one that succeeds ends the scan
|
|
141
|
+
transitions.each do |t|
|
|
142
|
+
next if cannot_transition?(t[:from], t[:when], current)
|
|
143
|
+
|
|
144
|
+
instance_variable_set(var_name, t[:to])
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
149
|
+
false
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def event_exists?(event_name)
|
|
154
|
+
event_name && !@events.include?(event_name)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
|
|
158
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
159
|
+
method_lambda = lambda {
|
|
160
|
+
if send(may_event_name)
|
|
161
|
+
instance_variable_set(var_name, to)
|
|
162
|
+
instance_exec(&after) if after
|
|
163
|
+
return true
|
|
164
|
+
end
|
|
165
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
166
|
+
false
|
|
167
|
+
}
|
|
168
|
+
make_owner_method event_name, method_lambda
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def setup_may_event_method(may_event_name, from, cond, guard)
|
|
172
|
+
state_machine_name = @name
|
|
173
|
+
#
|
|
174
|
+
# Instead of one "may_event?" method that checks all variations every time it's called, here we check
|
|
175
|
+
# the event definition and define the most optimal lambda to ensure the check is as fast as possible
|
|
176
|
+
method_lambda = if from == :any
|
|
177
|
+
from_any_may_event_lambda(guard, cond, state_machine_name)
|
|
178
|
+
else
|
|
179
|
+
guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
180
|
+
end
|
|
181
|
+
make_owner_method may_event_name, method_lambda
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def from_any_may_event_lambda(guard, cond, _state_machine_name)
|
|
185
|
+
if !guard && !cond
|
|
186
|
+
-> { true } # unguarded transition from any state
|
|
187
|
+
elsif !cond
|
|
188
|
+
guard # guarded transition from any state
|
|
189
|
+
elsif !guard
|
|
190
|
+
cond # conditional unguarded transition from any state
|
|
191
|
+
else
|
|
192
|
+
-> { instance_exec(&guard) && instance_exec(&cond) }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
197
|
+
if !guard && !cond
|
|
198
|
+
guardless_may_event_lambda(from, state_machine_name)
|
|
199
|
+
elsif !cond
|
|
200
|
+
guarded_may_event_lambda(from, guard, state_machine_name)
|
|
201
|
+
elsif !guard
|
|
202
|
+
guarded_may_event_lambda(from, cond, state_machine_name)
|
|
203
|
+
else
|
|
204
|
+
guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def guarded_may_event_lambda(from, guard, state_machine_name)
|
|
209
|
+
if from.is_a?(Array)
|
|
210
|
+
lambda { # guarded transition from choice of states
|
|
211
|
+
current = send(state_machine_name)
|
|
212
|
+
from.include?(current) && instance_exec(&guard)
|
|
213
|
+
}
|
|
214
|
+
else
|
|
215
|
+
lambda { # guarded transition from one state
|
|
216
|
+
current = send(state_machine_name)
|
|
217
|
+
from == current && instance_exec(&guard)
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
223
|
+
if from.is_a?(Array)
|
|
224
|
+
lambda { # guarded transition from choice of states
|
|
225
|
+
current = send(state_machine_name)
|
|
226
|
+
from.include?(current) && instance_exec(&guard) && instance_exec(&cond)
|
|
227
|
+
}
|
|
228
|
+
else
|
|
229
|
+
lambda { # guarded transition from one state
|
|
230
|
+
current = send(state_machine_name)
|
|
231
|
+
from == current && instance_exec(&guard) && instance_exec(&cond)
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def guardless_may_event_lambda(from, state_machine_name)
|
|
237
|
+
if from.is_a?(Array)
|
|
238
|
+
lambda { # unguarded transition from choice of states
|
|
239
|
+
current = send(state_machine_name)
|
|
240
|
+
from.include?(current)
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
lambda { # unguarded transition from one state
|
|
244
|
+
current = send(state_machine_name)
|
|
245
|
+
from == current
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def setup_base_methods
|
|
251
|
+
var_name = "@#{name}"
|
|
252
|
+
fsm = self
|
|
253
|
+
make_owner_method @name, lambda {
|
|
254
|
+
instance_variable_get(var_name) ||
|
|
255
|
+
fsm.initial_state
|
|
256
|
+
}
|
|
257
|
+
make_owner_method "#{@name}_states", -> { fsm.states }
|
|
258
|
+
make_owner_method "#{@name}_events", -> { fsm.events }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def make_owner_method(method_name, method_definition)
|
|
262
|
+
@owner_class.define_method(method_name, method_definition)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def state_match?(from, current)
|
|
269
|
+
return true if from == :any
|
|
270
|
+
return from.include?(current) if from.is_a?(Array)
|
|
271
|
+
|
|
272
|
+
from == current
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def cannot_transition?(from, cond, current)
|
|
276
|
+
(from && !state_match?(from, current)) || (cond && !instance_exec(&cond))
|
|
277
|
+
end
|
|
278
|
+
end
|
data/lib/simply_fsm/version.rb
CHANGED
data/lib/simply_fsm.rb
CHANGED
|
@@ -1,252 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "simply_fsm/version"
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
# Defines the `SimplyFSM` module
|
|
7
|
-
module SimplyFSM
|
|
8
|
-
def self.included(base)
|
|
9
|
-
base.extend(ClassMethods)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def state_match?(from, current)
|
|
13
|
-
return true if from == :any
|
|
14
|
-
return from.include?(current) if from.is_a?(Array)
|
|
15
|
-
|
|
16
|
-
from == current
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def cannot_transition?(from, cond, current)
|
|
20
|
-
(from && !state_match?(from, current)) || (cond && !instance_exec(&cond))
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
##
|
|
24
|
-
# Defines the constructor for defining a state machine
|
|
25
|
-
module ClassMethods
|
|
26
|
-
##
|
|
27
|
-
# Declare a state machine called +name+ which can then be defined
|
|
28
|
-
# by a DSL defined by the methods of `StateMachine`, with the following +opts+:
|
|
29
|
-
# - an optional +fail+ lambda that is called when any event fails to transition)
|
|
30
|
-
def state_machine(name, opts = {}, &block)
|
|
31
|
-
fsm = StateMachine.new(name, self, fail: opts[:fail])
|
|
32
|
-
fsm.instance_eval(&block)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
##
|
|
37
|
-
# The DSL for defining a state machine
|
|
38
|
-
class StateMachine
|
|
39
|
-
attr_reader :initial_state, :states, :events, :name, :full_name
|
|
40
|
-
|
|
41
|
-
def initialize(name, owner_class, fail: nil)
|
|
42
|
-
@owner_class = owner_class
|
|
43
|
-
@name = name.to_sym
|
|
44
|
-
@full_name = "#{owner_class.name}/#{name}"
|
|
45
|
-
@states = []
|
|
46
|
-
@events = []
|
|
47
|
-
@initial_state = nil
|
|
48
|
-
@fail_handler = fail
|
|
49
|
-
|
|
50
|
-
setup_base_methods
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
##
|
|
54
|
-
# Declare a supported +state_name+, and optionally specify one as the +initial+ state.
|
|
55
|
-
def state(state_name, initial: false)
|
|
56
|
-
return if state_name.nil? || @states.include?(state_name)
|
|
57
|
-
|
|
58
|
-
status = state_name.to_sym
|
|
59
|
-
state_machine_name = @name
|
|
60
|
-
@states << status
|
|
61
|
-
@initial_state = status if initial
|
|
62
|
-
|
|
63
|
-
make_owner_method "#{state_name}?", lambda {
|
|
64
|
-
send(state_machine_name) == status
|
|
65
|
-
}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
##
|
|
69
|
-
# Define an event by +event_name+
|
|
70
|
-
#
|
|
71
|
-
# - which +transitions+ as a hash with a +from+ state or array of states and the +to+ state,
|
|
72
|
-
# - an optional +guard+ lambda which must return true for the transition to occur,
|
|
73
|
-
# - an optional +fail+ lambda that is called when the transition fails (overrides top-level fail handler), and
|
|
74
|
-
# - an optional do block that is called +after+ the transition succeeds
|
|
75
|
-
def event(event_name, transitions:, guard: nil, fail: nil, &after)
|
|
76
|
-
return unless event_exists?(event_name) && transitions
|
|
77
|
-
|
|
78
|
-
@events << event_name
|
|
79
|
-
may_event_name = "may_#{event_name}?"
|
|
80
|
-
|
|
81
|
-
if transitions.is_a?(Array)
|
|
82
|
-
setup_multi_transition_may_event_method transitions: transitions, guard: guard,
|
|
83
|
-
may_event_name: may_event_name
|
|
84
|
-
setup_multi_transition_event_method event_name,
|
|
85
|
-
transitions: transitions, guard: guard,
|
|
86
|
-
var_name: "@#{@name}", fail: fail || @fail_handler
|
|
87
|
-
return
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
to = transitions[:to]
|
|
91
|
-
setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], guard
|
|
92
|
-
setup_event_method event_name, var_name: "@#{@name}",
|
|
93
|
-
may_event_name: may_event_name, to: to,
|
|
94
|
-
fail: fail || @fail_handler, &after
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
def setup_multi_transition_may_event_method(transitions:, guard:, may_event_name:)
|
|
100
|
-
state_machine_name = @name
|
|
101
|
-
|
|
102
|
-
make_owner_method may_event_name, lambda {
|
|
103
|
-
if !guard || instance_exec(&guard)
|
|
104
|
-
current = send(state_machine_name)
|
|
105
|
-
# Check each transition, and first one that succeeds ends the scan
|
|
106
|
-
transitions.each do |t|
|
|
107
|
-
next if cannot_transition?(t[:from], t[:when], current)
|
|
108
|
-
|
|
109
|
-
return true
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
false
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
|
|
117
|
-
state_machine_name = @name
|
|
118
|
-
make_owner_method event_name, lambda {
|
|
119
|
-
if !guard || instance_exec(&guard)
|
|
120
|
-
current = send(state_machine_name)
|
|
121
|
-
# Check each transition, and first one that succeeds ends the scan
|
|
122
|
-
transitions.each do |t|
|
|
123
|
-
next if cannot_transition?(t[:from], t[:when], current)
|
|
124
|
-
|
|
125
|
-
instance_variable_set(var_name, t[:to])
|
|
126
|
-
return true
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
instance_exec(&fail) if fail
|
|
130
|
-
false
|
|
131
|
-
}
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def event_exists?(event_name)
|
|
135
|
-
event_name && !@events.include?(event_name)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
|
|
139
|
-
method_lambda = lambda {
|
|
140
|
-
if send(may_event_name)
|
|
141
|
-
instance_variable_set(var_name, to)
|
|
142
|
-
instance_exec(&after) if after
|
|
143
|
-
return true
|
|
144
|
-
end
|
|
145
|
-
# unable to satisfy pre-conditions for the event
|
|
146
|
-
if fail
|
|
147
|
-
if fail.is_a?(String) || fail.is_a?(Symbol)
|
|
148
|
-
send(fail, event_name)
|
|
149
|
-
else
|
|
150
|
-
instance_exec(event_name, &fail)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
false
|
|
154
|
-
}
|
|
155
|
-
make_owner_method event_name, method_lambda
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def setup_may_event_method(may_event_name, from, cond, guard)
|
|
159
|
-
state_machine_name = @name
|
|
160
|
-
#
|
|
161
|
-
# Instead of one "may_event?" method that checks all variations every time it's called, here we check
|
|
162
|
-
# the event definition and define the most optimal lambda to ensure the check is as fast as possible
|
|
163
|
-
method_lambda = if from == :any
|
|
164
|
-
from_any_may_event_lambda(guard, cond, state_machine_name)
|
|
165
|
-
else
|
|
166
|
-
guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
167
|
-
end
|
|
168
|
-
make_owner_method may_event_name, method_lambda
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def from_any_may_event_lambda(guard, cond, _state_machine_name)
|
|
172
|
-
if !guard && !cond
|
|
173
|
-
-> { true } # unguarded transition from any state
|
|
174
|
-
elsif !cond
|
|
175
|
-
guard # guarded transition from any state
|
|
176
|
-
elsif !guard
|
|
177
|
-
cond # conditional unguarded transition from any state
|
|
178
|
-
else
|
|
179
|
-
-> { instance_exec(&guard) && instance_exec(&cond) }
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
184
|
-
if !guard && !cond
|
|
185
|
-
guardless_may_event_lambda(from, state_machine_name)
|
|
186
|
-
elsif !cond
|
|
187
|
-
guarded_may_event_lambda(from, guard, state_machine_name)
|
|
188
|
-
elsif !guard
|
|
189
|
-
guarded_may_event_lambda(from, cond, state_machine_name)
|
|
190
|
-
else
|
|
191
|
-
guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def guarded_may_event_lambda(from, guard, state_machine_name)
|
|
196
|
-
if from.is_a?(Array)
|
|
197
|
-
lambda { # guarded transition from choice of states
|
|
198
|
-
current = send(state_machine_name)
|
|
199
|
-
from.include?(current) && instance_exec(&guard)
|
|
200
|
-
}
|
|
201
|
-
else
|
|
202
|
-
lambda { # guarded transition from one state
|
|
203
|
-
current = send(state_machine_name)
|
|
204
|
-
from == current && instance_exec(&guard)
|
|
205
|
-
}
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
210
|
-
if from.is_a?(Array)
|
|
211
|
-
lambda { # guarded transition from choice of states
|
|
212
|
-
current = send(state_machine_name)
|
|
213
|
-
from.include?(current) && instance_exec(&guard) && instance_exec(&cond)
|
|
214
|
-
}
|
|
215
|
-
else
|
|
216
|
-
lambda { # guarded transition from one state
|
|
217
|
-
current = send(state_machine_name)
|
|
218
|
-
from == current && instance_exec(&guard) && instance_exec(&cond)
|
|
219
|
-
}
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def guardless_may_event_lambda(from, state_machine_name)
|
|
224
|
-
if from.is_a?(Array)
|
|
225
|
-
lambda { # unguarded transition from choice of states
|
|
226
|
-
current = send(state_machine_name)
|
|
227
|
-
from.include?(current)
|
|
228
|
-
}
|
|
229
|
-
else
|
|
230
|
-
lambda { # unguarded transition from one state
|
|
231
|
-
current = send(state_machine_name)
|
|
232
|
-
from == current
|
|
233
|
-
}
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def setup_base_methods
|
|
238
|
-
var_name = "@#{name}"
|
|
239
|
-
fsm = self
|
|
240
|
-
make_owner_method @name, lambda {
|
|
241
|
-
instance_variable_get(var_name) ||
|
|
242
|
-
fsm.initial_state
|
|
243
|
-
}
|
|
244
|
-
make_owner_method "#{@name}_states", -> { fsm.states }
|
|
245
|
-
make_owner_method "#{@name}_events", -> { fsm.events }
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def make_owner_method(method_name, method_definition)
|
|
249
|
-
@owner_class.define_method(method_name, method_definition)
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
end
|
|
4
|
+
require "simply_fsm/simply_fsm"
|
data/simply_fsm.gemspec
CHANGED
|
@@ -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
|
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.3.0
|
|
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-05-06 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
|
|
@@ -59,12 +74,14 @@ files:
|
|
|
59
74
|
- bin/console
|
|
60
75
|
- bin/setup
|
|
61
76
|
- lib/simply_fsm.rb
|
|
77
|
+
- lib/simply_fsm/simply_fsm.rb
|
|
62
78
|
- lib/simply_fsm/version.rb
|
|
63
79
|
- simply_fsm.gemspec
|
|
64
80
|
- spec/spec_helper.rb
|
|
65
81
|
- spec/support/state_machine_examples.rb
|
|
66
82
|
- spec/unit/fail_events_spec.rb
|
|
67
83
|
- spec/unit/guard_events_spec.rb
|
|
84
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
68
85
|
- spec/unit/multi_transition_state_machine_spec.rb
|
|
69
86
|
- spec/unit/one_state_machine_spec.rb
|
|
70
87
|
- spec/unit/simply_fsm_spec.rb
|
|
@@ -100,6 +117,7 @@ test_files:
|
|
|
100
117
|
- spec/support/state_machine_examples.rb
|
|
101
118
|
- spec/unit/fail_events_spec.rb
|
|
102
119
|
- spec/unit/guard_events_spec.rb
|
|
120
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
103
121
|
- spec/unit/multi_transition_state_machine_spec.rb
|
|
104
122
|
- spec/unit/one_state_machine_spec.rb
|
|
105
123
|
- spec/unit/simply_fsm_spec.rb
|