simply_fsm 0.1.1 → 0.2.1
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 +1 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +7 -1
- data/README.md +48 -16
- data/Rakefile +10 -0
- data/lib/simply_fsm/version.rb +1 -1
- data/lib/simply_fsm.rb +193 -70
- 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 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fbbc600cd621a9dc61fe1e27e7f1713ffa51e52719e13975e32aae5963ae5b2
|
|
4
|
+
data.tar.gz: ad107702004bbb2e877b71a55404c53294dc8cdad83dfecbdacbe5cb5bcf7ab1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c308a333334a16a02f1d885848c529ef4dc6066367c221cdb93c6ada4eb1fc909557e4376d340df13c34f81ddd03bca7e06f22cac0ca8f71a23c08e18818652a
|
|
7
|
+
data.tar.gz: b1775decefc14bc483fbe035d8d2b1f65957e07fc8bad9893b3a4c41b5deee0c01f54c67a63cdb32e5f111055bc65619476941981d333f21f520dd9ad8dc79f4
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 2.7
|
|
3
3
|
|
|
4
|
+
Naming/FileName:
|
|
5
|
+
Exclude:
|
|
6
|
+
- Rakefile
|
|
7
|
+
|
|
4
8
|
Style/StringLiterals:
|
|
5
9
|
Enabled: true
|
|
6
10
|
EnforcedStyle: double_quotes
|
|
@@ -11,3 +15,13 @@ Style/StringLiteralsInInterpolation:
|
|
|
11
15
|
|
|
12
16
|
Layout/LineLength:
|
|
13
17
|
Max: 120
|
|
18
|
+
|
|
19
|
+
Metrics/MethodLength:
|
|
20
|
+
Max: 16
|
|
21
|
+
|
|
22
|
+
Metrics/ClassLength:
|
|
23
|
+
Max: 256
|
|
24
|
+
|
|
25
|
+
Metrics/BlockLength:
|
|
26
|
+
Exclude:
|
|
27
|
+
- 'spec/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- None right now
|
|
4
|
+
|
|
5
|
+
## [0.2.1] - 2022-03-05
|
|
6
|
+
|
|
7
|
+
- Fixed bug where named fail handlers were not called properly for multi-transition events.
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2022-03-01
|
|
10
|
+
|
|
11
|
+
- *Breaks API* (sorry!)
|
|
12
|
+
- When declaring events in a state machine, use `transitions:` (as in "transitions from X to Y") instead of `transition:`
|
|
13
|
+
- Added support for multiple transitions per event
|
|
14
|
+
|
|
15
|
+
## [0.1.2] - 2022-02-28
|
|
16
|
+
|
|
17
|
+
- Cleaned up source with smaller clearer methods
|
|
18
|
+
- Added `rdoc` support, include `rake rdoc` task
|
|
19
|
+
|
|
3
20
|
## [0.1.1] - 2022-02-21
|
|
4
21
|
|
|
5
22
|
- Separated version file, fixed URLs in Gem spec, added badge to README
|
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.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -11,8 +11,12 @@ GEM
|
|
|
11
11
|
parallel (1.21.0)
|
|
12
12
|
parser (3.1.1.0)
|
|
13
13
|
ast (~> 2.4.1)
|
|
14
|
+
psych (4.0.3)
|
|
15
|
+
stringio
|
|
14
16
|
rainbow (3.1.1)
|
|
15
17
|
rake (13.0.6)
|
|
18
|
+
rdoc (6.4.0)
|
|
19
|
+
psych (>= 4.0.0)
|
|
16
20
|
regexp_parser (2.2.1)
|
|
17
21
|
rexml (3.2.5)
|
|
18
22
|
rspec (3.11.0)
|
|
@@ -40,6 +44,7 @@ GEM
|
|
|
40
44
|
rubocop-ast (1.15.2)
|
|
41
45
|
parser (>= 3.0.1.1)
|
|
42
46
|
ruby-progressbar (1.11.0)
|
|
47
|
+
stringio (3.0.1)
|
|
43
48
|
unicode-display_width (2.1.0)
|
|
44
49
|
|
|
45
50
|
PLATFORMS
|
|
@@ -47,6 +52,7 @@ PLATFORMS
|
|
|
47
52
|
|
|
48
53
|
DEPENDENCIES
|
|
49
54
|
rake (~> 13.0)
|
|
55
|
+
rdoc
|
|
50
56
|
rspec (~> 3.0)
|
|
51
57
|
rubocop (~> 1.21)
|
|
52
58
|
simply_fsm!
|
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
|
@@ -7,6 +7,16 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
7
7
|
|
|
8
8
|
require "rubocop/rake_task"
|
|
9
9
|
|
|
10
|
+
require "rdoc/task"
|
|
11
|
+
# require "simply_fsm/version"
|
|
12
|
+
|
|
13
|
+
Rake::RDocTask.new do |rdoc|
|
|
14
|
+
rdoc.rdoc_dir = "rdoc"
|
|
15
|
+
rdoc.title = "simply_fsm #{SimplyFSM::VERSION}"
|
|
16
|
+
rdoc.rdoc_files.include("README*")
|
|
17
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
10
20
|
RuboCop::RakeTask.new
|
|
11
21
|
|
|
12
22
|
task default: %i[spec rubocop]
|
data/lib/simply_fsm/version.rb
CHANGED
data/lib/simply_fsm.rb
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "simply_fsm/version"
|
|
4
4
|
|
|
5
|
+
##
|
|
6
|
+
# Defines the `SimplyFSM` module
|
|
5
7
|
module SimplyFSM
|
|
6
8
|
def self.included(base)
|
|
7
9
|
base.extend(ClassMethods)
|
|
8
10
|
end
|
|
9
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
|
|
10
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)
|
|
11
30
|
def state_machine(name, opts = {}, &block)
|
|
12
31
|
fsm = StateMachine.new(name, self, fail: opts[:fail])
|
|
13
32
|
fsm.instance_eval(&block)
|
|
14
33
|
end
|
|
15
34
|
end
|
|
16
35
|
|
|
36
|
+
##
|
|
37
|
+
# The DSL for defining a state machine
|
|
17
38
|
class StateMachine
|
|
18
39
|
attr_reader :initial_state, :states, :events, :name, :full_name
|
|
19
40
|
|
|
@@ -29,93 +50,195 @@ module SimplyFSM
|
|
|
29
50
|
setup_base_methods
|
|
30
51
|
end
|
|
31
52
|
|
|
53
|
+
##
|
|
54
|
+
# Declare a supported +state_name+, and optionally specify one as the +initial+ state.
|
|
32
55
|
def state(state_name, initial: false)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
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
|
|
43
95
|
end
|
|
44
96
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
# Setup the event method to attempt to make the state
|
|
59
|
-
# transition or report failure
|
|
60
|
-
make_owner_method event_name, lambda {
|
|
61
|
-
if send(may_event_name)
|
|
62
|
-
instance_variable_set(var_name, to)
|
|
63
|
-
instance_exec(&after) if after
|
|
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
|
+
|
|
64
109
|
return true
|
|
65
110
|
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
74
|
-
false
|
|
75
|
-
}
|
|
111
|
+
end
|
|
112
|
+
false
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def setup_fail_lambda_for(fail)
|
|
117
|
+
return unless fail
|
|
76
118
|
|
|
119
|
+
if fail.is_a?(String) || fail.is_a?(Symbol)
|
|
120
|
+
->(event_name) { send(fail, event_name) }
|
|
121
|
+
else
|
|
122
|
+
->(event_name) { instance_exec(event_name, &fail) }
|
|
77
123
|
end
|
|
78
124
|
end
|
|
79
125
|
|
|
80
|
-
|
|
126
|
+
def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
|
|
127
|
+
state_machine_name = @name
|
|
128
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
129
|
+
make_owner_method event_name, lambda {
|
|
130
|
+
if !guard || instance_exec(&guard)
|
|
131
|
+
current = send(state_machine_name)
|
|
132
|
+
# Check each transition, and first one that succeeds ends the scan
|
|
133
|
+
transitions.each do |t|
|
|
134
|
+
next if cannot_transition?(t[:from], t[:when], current)
|
|
135
|
+
|
|
136
|
+
instance_variable_set(var_name, t[:to])
|
|
137
|
+
return true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
141
|
+
false
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def event_exists?(event_name)
|
|
146
|
+
event_name && !@events.include?(event_name)
|
|
147
|
+
end
|
|
81
148
|
|
|
82
|
-
def
|
|
149
|
+
def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
|
|
150
|
+
fail_lambda = setup_fail_lambda_for(fail)
|
|
151
|
+
method_lambda = lambda {
|
|
152
|
+
if send(may_event_name)
|
|
153
|
+
instance_variable_set(var_name, to)
|
|
154
|
+
instance_exec(&after) if after
|
|
155
|
+
return true
|
|
156
|
+
end
|
|
157
|
+
instance_exec(event_name, &fail_lambda) if fail_lambda
|
|
158
|
+
false
|
|
159
|
+
}
|
|
160
|
+
make_owner_method event_name, method_lambda
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def setup_may_event_method(may_event_name, from, cond, guard)
|
|
83
164
|
state_machine_name = @name
|
|
84
165
|
#
|
|
85
|
-
# Instead of one "may_event?" method that checks all variations
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
method_lambda = if from == :any && !guard
|
|
90
|
-
-> { true } # unguarded transition from any state
|
|
91
|
-
elsif from == :any
|
|
92
|
-
guard # guarded transition from any state
|
|
93
|
-
elsif !guard
|
|
94
|
-
if from.is_a?(Array)
|
|
95
|
-
lambda { # unguarded transition from choice of states
|
|
96
|
-
current = send(state_machine_name)
|
|
97
|
-
from.include?(current)
|
|
98
|
-
}
|
|
99
|
-
else
|
|
100
|
-
lambda { # unguarded transition from one state
|
|
101
|
-
current = send(state_machine_name)
|
|
102
|
-
from == current
|
|
103
|
-
}
|
|
104
|
-
end
|
|
105
|
-
elsif from.is_a?(Array)
|
|
106
|
-
lambda { # guarded transition from choice of states
|
|
107
|
-
current = send(state_machine_name)
|
|
108
|
-
from.include?(current) && instance_exec(&guard)
|
|
109
|
-
}
|
|
166
|
+
# Instead of one "may_event?" method that checks all variations every time it's called, here we check
|
|
167
|
+
# the event definition and define the most optimal lambda to ensure the check is as fast as possible
|
|
168
|
+
method_lambda = if from == :any
|
|
169
|
+
from_any_may_event_lambda(guard, cond, state_machine_name)
|
|
110
170
|
else
|
|
111
|
-
|
|
112
|
-
current = send(state_machine_name)
|
|
113
|
-
from == current && instance_exec(&guard)
|
|
114
|
-
}
|
|
171
|
+
guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
115
172
|
end
|
|
116
173
|
make_owner_method may_event_name, method_lambda
|
|
117
174
|
end
|
|
118
175
|
|
|
176
|
+
def from_any_may_event_lambda(guard, cond, _state_machine_name)
|
|
177
|
+
if !guard && !cond
|
|
178
|
+
-> { true } # unguarded transition from any state
|
|
179
|
+
elsif !cond
|
|
180
|
+
guard # guarded transition from any state
|
|
181
|
+
elsif !guard
|
|
182
|
+
cond # conditional unguarded transition from any state
|
|
183
|
+
else
|
|
184
|
+
-> { instance_exec(&guard) && instance_exec(&cond) }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
189
|
+
if !guard && !cond
|
|
190
|
+
guardless_may_event_lambda(from, state_machine_name)
|
|
191
|
+
elsif !cond
|
|
192
|
+
guarded_may_event_lambda(from, guard, state_machine_name)
|
|
193
|
+
elsif !guard
|
|
194
|
+
guarded_may_event_lambda(from, cond, state_machine_name)
|
|
195
|
+
else
|
|
196
|
+
guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def guarded_may_event_lambda(from, guard, state_machine_name)
|
|
201
|
+
if from.is_a?(Array)
|
|
202
|
+
lambda { # guarded transition from choice of states
|
|
203
|
+
current = send(state_machine_name)
|
|
204
|
+
from.include?(current) && instance_exec(&guard)
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
lambda { # guarded transition from one state
|
|
208
|
+
current = send(state_machine_name)
|
|
209
|
+
from == current && instance_exec(&guard)
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
215
|
+
if from.is_a?(Array)
|
|
216
|
+
lambda { # guarded transition from choice of states
|
|
217
|
+
current = send(state_machine_name)
|
|
218
|
+
from.include?(current) && instance_exec(&guard) && instance_exec(&cond)
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
lambda { # guarded transition from one state
|
|
222
|
+
current = send(state_machine_name)
|
|
223
|
+
from == current && instance_exec(&guard) && instance_exec(&cond)
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def guardless_may_event_lambda(from, state_machine_name)
|
|
229
|
+
if from.is_a?(Array)
|
|
230
|
+
lambda { # unguarded transition from choice of states
|
|
231
|
+
current = send(state_machine_name)
|
|
232
|
+
from.include?(current)
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
lambda { # unguarded transition from one state
|
|
236
|
+
current = send(state_machine_name)
|
|
237
|
+
from == current
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
119
242
|
def setup_base_methods
|
|
120
243
|
var_name = "@#{name}"
|
|
121
244
|
fsm = self
|
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,15 +1,29 @@
|
|
|
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.1
|
|
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-03-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rdoc
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: rspec
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +65,8 @@ files:
|
|
|
51
65
|
- spec/support/state_machine_examples.rb
|
|
52
66
|
- spec/unit/fail_events_spec.rb
|
|
53
67
|
- spec/unit/guard_events_spec.rb
|
|
68
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
69
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
54
70
|
- spec/unit/one_state_machine_spec.rb
|
|
55
71
|
- spec/unit/simply_fsm_spec.rb
|
|
56
72
|
- spec/unit/two_state_machines_spec.rb
|
|
@@ -76,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
92
|
- !ruby/object:Gem::Version
|
|
77
93
|
version: '0'
|
|
78
94
|
requirements: []
|
|
79
|
-
rubygems_version: 3.
|
|
95
|
+
rubygems_version: 3.3.7
|
|
80
96
|
signing_key:
|
|
81
97
|
specification_version: 4
|
|
82
98
|
summary: Simple finite state mechine (FSM) data-type mixin for Ruby objects.
|
|
@@ -85,6 +101,8 @@ test_files:
|
|
|
85
101
|
- spec/support/state_machine_examples.rb
|
|
86
102
|
- spec/unit/fail_events_spec.rb
|
|
87
103
|
- spec/unit/guard_events_spec.rb
|
|
104
|
+
- spec/unit/multi_transition_fail_events_spec.rb
|
|
105
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
88
106
|
- spec/unit/one_state_machine_spec.rb
|
|
89
107
|
- spec/unit/simply_fsm_spec.rb
|
|
90
108
|
- spec/unit/two_state_machines_spec.rb
|