simply_fsm 0.1.2 → 0.2.3

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