simply_fsm 0.1.0 → 0.2.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 +1 -0
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile.lock +7 -1
- data/README.md +50 -16
- data/Rakefile +10 -0
- data/lib/simply_fsm/version.rb +5 -0
- data/lib/simply_fsm.rb +189 -71
- data/simply_fsm.gemspec +5 -4
- data/spec/unit/fail_events_spec.rb +3 -3
- data/spec/unit/guard_events_spec.rb +6 -6
- 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 +23 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb54d24c49d69bc4cb382e6c4181a586fe9eef471dd1e44bcef0d95555fb33ae
|
|
4
|
+
data.tar.gz: 2f336e971e10827c1b731948376b4f2a177e012093c3f66457380df21c0f110c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9afdd28a4281a6d6c4d40abe7d52e5ab674e9bf1e626a9bc2bf47ee678e047f0fd3c4e5ab3f6857a01fc6fd6647a5eab416a6cc1c03f7ebbbdda1a72b6b1d71b
|
|
7
|
+
data.tar.gz: 21c938cbced1efb61d61c0cc62c4887d1f8084c4b42c6bc2effd0cda358a9cbade73a958250fdb9d81a17a183ed2b092e7d6896ae043fc1dfcc2afa7c9914134
|
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.0] - 2022-03-01
|
|
6
|
+
|
|
7
|
+
- *Breaks API* (sorry!)
|
|
8
|
+
- When declaring events in a state machine, use `transitions:` (as in "transitions from X to Y") instead of `transition:`
|
|
9
|
+
- Added support for multiple transitions per event
|
|
10
|
+
|
|
11
|
+
## [0.1.2] - 2022-02-28
|
|
12
|
+
|
|
13
|
+
- Cleaned up source with smaller clearer methods
|
|
14
|
+
- Added `rdoc` support, include `rake rdoc` task
|
|
15
|
+
|
|
16
|
+
## [0.1.1] - 2022-02-21
|
|
17
|
+
|
|
18
|
+
- Separated version file, fixed URLs in Gem spec, added badge to README
|
|
19
|
+
|
|
3
20
|
## [0.1.0] - 2022-02-21
|
|
4
21
|
|
|
5
22
|
- Initial release supports finite state machine with event guards and failure handling
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
simply_fsm (0.1.
|
|
4
|
+
simply_fsm (0.1.1)
|
|
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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# SimplyFSM
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/simply_fsm)
|
|
4
|
+
|
|
3
5
|
`simply_fsm` is a bare-necessities finite state machine data-type for use with any Ruby class. I created `simply_fsm` because I wanted the minimal FSM data type that was easy to use and did everything I would expect from a core data type.
|
|
4
6
|
|
|
5
7
|
If you need storage/persistence/Rails/etc support, I recommend [AASM](https://github.com/aasm/aasm) whose API was an inspiration for this gem.
|
|
@@ -11,6 +13,7 @@ If you need storage/persistence/Rails/etc support, I recommend [AASM](https://gi
|
|
|
11
13
|
- [Multiple state machines](#multiple-state-machines)
|
|
12
14
|
- [Handle failed events](#handle-failed-events)
|
|
13
15
|
- [Guarding events](#guarding-events)
|
|
16
|
+
- [Multiple transitions for an event](#multiple-transitions-for-an-event)
|
|
14
17
|
- [Development](#development)
|
|
15
18
|
- [Contributing](#contributing)
|
|
16
19
|
- [License](#license)
|
|
@@ -47,15 +50,15 @@ class Job
|
|
|
47
50
|
state :running
|
|
48
51
|
state :cleaning
|
|
49
52
|
|
|
50
|
-
event :run,
|
|
53
|
+
event :run, transitions: { from: :sleeping, to: :running } do
|
|
51
54
|
# executed when transition succeeds
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
event :clean,
|
|
57
|
+
event :clean, transitions: { from: :running, to: :cleaning } do
|
|
55
58
|
# do the cleaning since transition succeeded
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
event :sleep,
|
|
61
|
+
event :sleep, transitions: { from: [:running, :cleaning], to: :sleeping }
|
|
59
62
|
end
|
|
60
63
|
end
|
|
61
64
|
```
|
|
@@ -86,16 +89,16 @@ class Player
|
|
|
86
89
|
state :idling, initial: true
|
|
87
90
|
state :walking
|
|
88
91
|
|
|
89
|
-
event :idle,
|
|
90
|
-
event :walk,
|
|
92
|
+
event :idle, transitions: { from: :any, to: :idling }
|
|
93
|
+
event :walk, transitions: { from: :idling, to: :walking }
|
|
91
94
|
end
|
|
92
95
|
|
|
93
96
|
state_machine :action do
|
|
94
97
|
state :ready, initial: true
|
|
95
98
|
state :blocking
|
|
96
99
|
|
|
97
|
-
event :hold,
|
|
98
|
-
event :block,
|
|
100
|
+
event :hold, transitions: { from: :any, to: :ready }
|
|
101
|
+
event :block, transitions: { from: :any, to: :blocking }
|
|
99
102
|
end
|
|
100
103
|
end
|
|
101
104
|
```
|
|
@@ -124,11 +127,11 @@ class JobWithErrors
|
|
|
124
127
|
state :running
|
|
125
128
|
state :cleaning
|
|
126
129
|
|
|
127
|
-
event :sleep,
|
|
128
|
-
event :clean,
|
|
130
|
+
event :sleep, transitions: { from: %i[running cleaning], to: :sleeping }
|
|
131
|
+
event :clean, transitions: { from: :running, to: :cleaning }
|
|
129
132
|
event :run,
|
|
130
133
|
fail: ->(_event) { raise RunError, "Cannot run" },
|
|
131
|
-
|
|
134
|
+
transitions: { from: :sleeping, to: :running }
|
|
132
135
|
end
|
|
133
136
|
|
|
134
137
|
def on_any_fail(event_name)
|
|
@@ -152,9 +155,9 @@ class AgilePlayer
|
|
|
152
155
|
state :walking
|
|
153
156
|
state :running
|
|
154
157
|
|
|
155
|
-
event :idle,
|
|
156
|
-
event :walk,
|
|
157
|
-
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 }
|
|
158
161
|
end
|
|
159
162
|
|
|
160
163
|
state_machine :action do
|
|
@@ -162,14 +165,45 @@ class AgilePlayer
|
|
|
162
165
|
state :jumping
|
|
163
166
|
state :leaping
|
|
164
167
|
|
|
165
|
-
event :hold,
|
|
168
|
+
event :hold, transitions: { from: :any, to: :ready }
|
|
166
169
|
event :jump,
|
|
167
170
|
guard: -> { !running? },
|
|
168
|
-
|
|
171
|
+
transitions: { from: :ready, to: :jumping }
|
|
169
172
|
event :leap,
|
|
170
173
|
guard: -> { running? },
|
|
171
174
|
fail: ->(_event) { raise LeapError, "Cannot leap" },
|
|
172
|
-
|
|
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
|
+
]
|
|
173
207
|
end
|
|
174
208
|
end
|
|
175
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.rb
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
VERSION = "0.1.0"
|
|
3
|
+
require "simply_fsm/version"
|
|
5
4
|
|
|
5
|
+
##
|
|
6
|
+
# Defines the `SimplyFSM` module
|
|
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,190 @@ 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
|
-
|
|
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
|
|
73
127
|
end
|
|
74
|
-
|
|
75
|
-
|
|
128
|
+
end
|
|
129
|
+
instance_exec(&fail) if fail
|
|
130
|
+
false
|
|
131
|
+
}
|
|
132
|
+
end
|
|
76
133
|
|
|
77
|
-
|
|
134
|
+
def event_exists?(event_name)
|
|
135
|
+
event_name && !@events.include?(event_name)
|
|
78
136
|
end
|
|
79
137
|
|
|
80
|
-
|
|
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
|
|
81
157
|
|
|
82
|
-
def setup_may_event_method(may_event_name, from,
|
|
158
|
+
def setup_may_event_method(may_event_name, from, cond, guard)
|
|
83
159
|
state_machine_name = @name
|
|
84
160
|
#
|
|
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
|
-
}
|
|
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)
|
|
110
165
|
else
|
|
111
|
-
|
|
112
|
-
current = send(state_machine_name)
|
|
113
|
-
from == current && instance_exec(&guard)
|
|
114
|
-
}
|
|
166
|
+
guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
|
|
115
167
|
end
|
|
116
168
|
make_owner_method may_event_name, method_lambda
|
|
117
169
|
end
|
|
118
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
|
+
|
|
119
237
|
def setup_base_methods
|
|
120
238
|
var_name = "@#{name}"
|
|
121
239
|
fsm = self
|
data/simply_fsm.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "lib/simply_fsm"
|
|
3
|
+
require_relative "lib/simply_fsm/version"
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "simply_fsm"
|
|
@@ -10,14 +10,14 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "Simple finite state mechine (FSM) data-type mixin for Ruby objects."
|
|
12
12
|
spec.description = "Use it to setup one or more FSMs in any Ruby object."
|
|
13
|
-
spec.homepage = "https://github.com/nogginly/simply_fsm"
|
|
13
|
+
spec.homepage = "https://github.com/nogginly/simply_fsm#simplyfsm"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
spec.required_ruby_version = ">= 2.7.0"
|
|
16
16
|
spec.date = Time.now
|
|
17
17
|
|
|
18
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
-
spec.metadata["source_code_uri"] =
|
|
20
|
-
spec.metadata["changelog_uri"] = "https://github.com/nogginly/simply_fsm/CHANGELOG.md"
|
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/nogginly/simply_fsm"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/nogginly/simply_fsm/blob/main/CHANGELOG.md"
|
|
21
21
|
|
|
22
22
|
spec.files = `git ls-files`.split("\n")
|
|
23
23
|
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
@@ -26,5 +26,6 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
# no deployment dependencies
|
|
27
27
|
|
|
28
28
|
# development
|
|
29
|
+
spec.add_development_dependency "rdoc"
|
|
29
30
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
30
31
|
end
|
|
@@ -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,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.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-03-01 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
|
|
@@ -45,21 +59,23 @@ files:
|
|
|
45
59
|
- bin/console
|
|
46
60
|
- bin/setup
|
|
47
61
|
- lib/simply_fsm.rb
|
|
62
|
+
- lib/simply_fsm/version.rb
|
|
48
63
|
- simply_fsm.gemspec
|
|
49
64
|
- spec/spec_helper.rb
|
|
50
65
|
- spec/support/state_machine_examples.rb
|
|
51
66
|
- spec/unit/fail_events_spec.rb
|
|
52
67
|
- spec/unit/guard_events_spec.rb
|
|
68
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
53
69
|
- spec/unit/one_state_machine_spec.rb
|
|
54
70
|
- spec/unit/simply_fsm_spec.rb
|
|
55
71
|
- spec/unit/two_state_machines_spec.rb
|
|
56
|
-
homepage: https://github.com/nogginly/simply_fsm
|
|
72
|
+
homepage: https://github.com/nogginly/simply_fsm#simplyfsm
|
|
57
73
|
licenses:
|
|
58
74
|
- MIT
|
|
59
75
|
metadata:
|
|
60
|
-
homepage_uri: https://github.com/nogginly/simply_fsm
|
|
76
|
+
homepage_uri: https://github.com/nogginly/simply_fsm#simplyfsm
|
|
61
77
|
source_code_uri: https://github.com/nogginly/simply_fsm
|
|
62
|
-
changelog_uri: https://github.com/nogginly/simply_fsm/CHANGELOG.md
|
|
78
|
+
changelog_uri: https://github.com/nogginly/simply_fsm/blob/main/CHANGELOG.md
|
|
63
79
|
post_install_message:
|
|
64
80
|
rdoc_options: []
|
|
65
81
|
require_paths:
|
|
@@ -75,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
75
91
|
- !ruby/object:Gem::Version
|
|
76
92
|
version: '0'
|
|
77
93
|
requirements: []
|
|
78
|
-
rubygems_version: 3.
|
|
94
|
+
rubygems_version: 3.3.7
|
|
79
95
|
signing_key:
|
|
80
96
|
specification_version: 4
|
|
81
97
|
summary: Simple finite state mechine (FSM) data-type mixin for Ruby objects.
|
|
@@ -84,6 +100,7 @@ test_files:
|
|
|
84
100
|
- spec/support/state_machine_examples.rb
|
|
85
101
|
- spec/unit/fail_events_spec.rb
|
|
86
102
|
- spec/unit/guard_events_spec.rb
|
|
103
|
+
- spec/unit/multi_transition_state_machine_spec.rb
|
|
87
104
|
- spec/unit/one_state_machine_spec.rb
|
|
88
105
|
- spec/unit/simply_fsm_spec.rb
|
|
89
106
|
- spec/unit/two_state_machines_spec.rb
|