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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b86e3a94184036d2ba485db8452bcf2c4ad3c8314e71b56b481f1a428d253ad
4
- data.tar.gz: 445b68adba6dc1d0376473288af06a04026bdeb055616c0ef3a31acb6f1b068d
3
+ metadata.gz: 0fbbc600cd621a9dc61fe1e27e7f1713ffa51e52719e13975e32aae5963ae5b2
4
+ data.tar.gz: ad107702004bbb2e877b71a55404c53294dc8cdad83dfecbdacbe5cb5bcf7ab1
5
5
  SHA512:
6
- metadata.gz: c754997ecccc818fab23c9079cf6f4f274cd8ab484ea56af7eee49ebc69ce97985814cb0ef0910088e339e8839b6fc4ab8f07848b014313c1106f8f8f310cdb0
7
- data.tar.gz: e11e94844f8a0e6dcad77b4cc8dc642afeced63c31678d1ac0278d19825e7e194062fa3d569381bac36be9465f85ccf1f500355a08681372cf455a43f03dbbf9
6
+ metadata.gz: c308a333334a16a02f1d885848c529ef4dc6066367c221cdb93c6ada4eb1fc909557e4376d340df13c34f81ddd03bca7e06f22cac0ca8f71a23c08e18818652a
7
+ data.tar.gz: b1775decefc14bc483fbe035d8d2b1f65957e07fc8bad9893b3a4c41b5deee0c01f54c67a63cdb32e5f111055bc65619476941981d333f21f520dd9ad8dc79f4
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  /_yardoc/
4
4
  /coverage/
5
5
  /doc/
6
+ /rdoc/
6
7
  /pkg/
7
8
  /spec/reports/
8
9
  /tmp/
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.1.1)
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, transition: { from: :sleeping, to: :running } do
53
+ event :run, transitions: { from: :sleeping, to: :running } do
53
54
  # executed when transition succeeds
54
55
  end
55
56
 
56
- event :clean, transition: { from: :running, to: :cleaning } do
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, transition: { from: [:running, :cleaning], to: :sleeping }
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, transition: { from: :any, to: :idling }
92
- event :walk, transition: { from: :idling, to: :walking }
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, transition: { from: :any, to: :ready }
100
- event :block, transition: { from: :any, to: :blocking }
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, transition: { from: %i[running cleaning], to: :sleeping }
130
- event :clean, transition: { from: :running, to: :cleaning }
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
- transition: { from: :sleeping, to: :running }
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, transition: { from: :any, to: :idling }
158
- event :walk, transition: { from: :any, to: :walking }
159
- event :run, transition: { from: :any, to: :running }
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, transition: { from: :any, to: :ready }
168
+ event :hold, transitions: { from: :any, to: :ready }
168
169
  event :jump,
169
170
  guard: -> { !running? },
170
- transition: { from: :ready, to: :jumping }
171
+ transitions: { from: :ready, to: :jumping }
171
172
  event :leap,
172
173
  guard: -> { running? },
173
174
  fail: ->(_event) { raise LeapError, "Cannot leap" },
174
- transition: { from: :ready, to: :leaping }
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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimplyFSM
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/simply_fsm.rb CHANGED
@@ -1,19 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'simply_fsm/version'
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
- unless state_name.nil? || @states.include?(state_name)
34
- status = state_name.to_sym
35
- state_machine_name = @name
36
- @states << status
37
- @initial_state = status if initial
38
-
39
- make_owner_method "#{state_name}?", lambda {
40
- send(state_machine_name) == status
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
- def event(event_name, transition:, guard: nil, fail: nil, &after)
46
- if event_name && transition
47
- @events << event_name
48
- from = transition[:from]
49
- to = transition[:to]
50
- state_machine_name = @name
51
- var_name = "@#{state_machine_name}"
52
- may_event_name = "may_#{event_name}?"
53
- fail = @fail_handler if fail.nil?
54
-
55
- setup_may_event_method may_event_name, from, to, guard
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
- # unable to satisfy pre-conditions for the event
67
- if fail
68
- if fail.is_a?(String) || fail.is_a?(Symbol)
69
- send(fail, event_name)
70
- else
71
- instance_exec(event_name, &fail)
72
- end
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
- private
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 setup_may_event_method(may_event_name, from, _to, guard)
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
- # every time it's called, here we check the event definition and
87
- # define the most optimal lambda to ensure the check is as fast as
88
- # possible
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
- lambda { # guarded transition from one state
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
@@ -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, transition: { from: %i[running cleaning], to: :sleeping }
15
- event :clean, transition: { from: :running, to: :cleaning }
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
- transition: { from: :sleeping, to: :running }
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, transition: { from: :any, to: :idling }
14
- event :walk, transition: { from: :any, to: :walking }
15
- event :run, transition: { from: :any, to: :running }
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, transition: { from: :any, to: :ready }
23
+ event :hold, transitions: { to: :ready }
24
24
  event :jump,
25
25
  guard: -> { !running? },
26
- transition: { from: :ready, to: :jumping }
26
+ transitions: { from: :ready, to: :jumping }
27
27
  event :leap,
28
28
  guard: -> { running? },
29
29
  fail: ->(_event) { raise LeapError, "Cannot leap" },
30
- transition: { from: :ready, to: :leaping }
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, transition: { from: :sleeping, to: :running }
12
- event :clean, transition: { from: :running, to: :cleaning }
13
- event :sleep, transition: { from: %i[running cleaning], to: :sleeping }
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, transition: { from: :any, to: :idling }
11
- event :walk, transition: { from: :idling, to: :walking }
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, transition: { from: :any, to: :ready }
19
- event :block, transition: { from: :any, to: :blocking }
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.1.1
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-02-21 00:00:00.000000000 Z
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.1.6
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