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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba0392debc9515f01b6b95bca88ed5c3a4aace120a3b725c4d1a840854a1725
4
- data.tar.gz: 1d7883abe1ce6b0db890203293ea4f3ff2ab275fb876628daf07fb0f9ea087d6
3
+ metadata.gz: eb54d24c49d69bc4cb382e6c4181a586fe9eef471dd1e44bcef0d95555fb33ae
4
+ data.tar.gz: 2f336e971e10827c1b731948376b4f2a177e012093c3f66457380df21c0f110c
5
5
  SHA512:
6
- metadata.gz: 9320692222abae9ec39bebf5b5594f023d97efecec721d400f6aacb5ed758cf0e02d23fc4e5845d604a0ec39682de16d7e919d8024ce69f762722cf134d1243b
7
- data.tar.gz: c1552d33ce0b9e1d682e7e1d7cbbc7cd6ec59fbc4435d63582f67735f6a75cb0fe114910266624aee241f694aa77c2031634351466b798563981fc71f112ef71
6
+ metadata.gz: 9afdd28a4281a6d6c4d40abe7d52e5ab674e9bf1e626a9bc2bf47ee678e047f0fd3c4e5ab3f6857a01fc6fd6647a5eab416a6cc1c03f7ebbbdda1a72b6b1d71b
7
+ data.tar.gz: 21c938cbced1efb61d61c0cc62c4887d1f8084c4b42c6bc2effd0cda358a9cbade73a958250fdb9d81a17a183ed2b092e7d6896ae043fc1dfcc2afa7c9914134
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.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.0)
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
+ [![Gem Version](https://badge.fury.io/rb/simply_fsm.svg)](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, transition: { from: :sleeping, to: :running } do
53
+ event :run, transitions: { from: :sleeping, to: :running } do
51
54
  # executed when transition succeeds
52
55
  end
53
56
 
54
- event :clean, transition: { from: :running, to: :cleaning } do
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, transition: { from: [:running, :cleaning], to: :sleeping }
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, transition: { from: :any, to: :idling }
90
- event :walk, transition: { from: :idling, to: :walking }
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, transition: { from: :any, to: :ready }
98
- event :block, transition: { from: :any, to: :blocking }
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, transition: { from: %i[running cleaning], to: :sleeping }
128
- 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 }
129
132
  event :run,
130
133
  fail: ->(_event) { raise RunError, "Cannot run" },
131
- transition: { from: :sleeping, to: :running }
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, transition: { from: :any, to: :idling }
156
- event :walk, transition: { from: :any, to: :walking }
157
- 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 }
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, transition: { from: :any, to: :ready }
168
+ event :hold, transitions: { from: :any, to: :ready }
166
169
  event :jump,
167
170
  guard: -> { !running? },
168
- transition: { from: :ready, to: :jumping }
171
+ transitions: { from: :ready, to: :jumping }
169
172
  event :leap,
170
173
  guard: -> { running? },
171
174
  fail: ->(_event) { raise LeapError, "Cannot leap" },
172
- 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
+ ]
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]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimplyFSM
4
+ VERSION = "0.2.0"
5
+ end
data/lib/simply_fsm.rb CHANGED
@@ -1,19 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SimplyFSM
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
- 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
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
- false
75
- }
128
+ end
129
+ instance_exec(&fail) if fail
130
+ false
131
+ }
132
+ end
76
133
 
77
- end
134
+ def event_exists?(event_name)
135
+ event_name && !@events.include?(event_name)
78
136
  end
79
137
 
80
- private
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, _to, guard)
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
- # 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
- }
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
- lambda { # guarded transition from one state
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"] = spec.homepage
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, 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,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.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-02-21 00:00:00.000000000 Z
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.1.6
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