simply_fsm 0.1.2 → 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: e86bd7cca4df06c4ada7a53528e2e0f5853fc9055bf0bb808c4cd029b552ad37
4
- data.tar.gz: 6ad8c2cfe781b729316f0df6707be5c10c17e0ed4e865a7042c22504e2039e0c
3
+ metadata.gz: eb54d24c49d69bc4cb382e6c4181a586fe9eef471dd1e44bcef0d95555fb33ae
4
+ data.tar.gz: 2f336e971e10827c1b731948376b4f2a177e012093c3f66457380df21c0f110c
5
5
  SHA512:
6
- metadata.gz: c3dea4db713c9cc75ef314ba8c536d7a0847fa396f12af0e69b951eb78a2bb20c3ed6f620aa1c11cb26f23087c93891ccf6d5cc78fd17abc4e2f0ab130819748
7
- data.tar.gz: 659bf78f96bf5bd41cc9cf03c50387d165c04148de5cfc6ea89955f18707f72ab2cfec271d36e2424622a528783b6a13dd88222ee695ce65497d0e930f8df603
6
+ metadata.gz: 9afdd28a4281a6d6c4d40abe7d52e5ab674e9bf1e626a9bc2bf47ee678e047f0fd3c4e5ab3f6857a01fc6fd6647a5eab416a6cc1c03f7ebbbdda1a72b6b1d71b
7
+ data.tar.gz: 21c938cbced1efb61d61c0cc62c4887d1f8084c4b42c6bc2effd0cda358a9cbade73a958250fdb9d81a17a183ed2b092e7d6896ae043fc1dfcc2afa7c9914134
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  - None right now
4
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
+
5
11
  ## [0.1.2] - 2022-02-28
6
12
 
7
13
  - Cleaned up source with smaller clearer methods
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
  ```
@@ -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.0"
5
5
  end
data/lib/simply_fsm.rb CHANGED
@@ -9,6 +9,17 @@ module SimplyFSM
9
9
  base.extend(ClassMethods)
10
10
  end
11
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
+
12
23
  ##
13
24
  # Defines the constructor for defining a state machine
14
25
  module ClassMethods
@@ -55,19 +66,29 @@ module SimplyFSM
55
66
  end
56
67
 
57
68
  ##
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,
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,
60
72
  # - an optional +guard+ lambda which must return true for the transition to occur,
61
73
  # - an optional +fail+ lambda that is called when the transition fails (overrides top-level fail handler), and
62
74
  # - 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
75
+ def event(event_name, transitions:, guard: nil, fail: nil, &after)
76
+ return unless event_exists?(event_name) && transitions
65
77
 
66
78
  @events << event_name
67
- to = transition[:to]
68
79
  may_event_name = "may_#{event_name}?"
69
80
 
70
- setup_may_event_method may_event_name, transition[:from], to, guard
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
88
+ end
89
+
90
+ to = transitions[:to]
91
+ setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], guard
71
92
  setup_event_method event_name, var_name: "@#{@name}",
72
93
  may_event_name: may_event_name, to: to,
73
94
  fail: fail || @fail_handler, &after
@@ -75,6 +96,41 @@ module SimplyFSM
75
96
 
76
97
  private
77
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
+
109
+ return true
110
+ 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
127
+ end
128
+ end
129
+ instance_exec(&fail) if fail
130
+ false
131
+ }
132
+ end
133
+
78
134
  def event_exists?(event_name)
79
135
  event_name && !@events.include?(event_name)
80
136
  end
@@ -99,23 +155,43 @@ module SimplyFSM
99
155
  make_owner_method event_name, method_lambda
100
156
  end
101
157
 
102
- def setup_may_event_method(may_event_name, from, _to, guard)
158
+ def setup_may_event_method(may_event_name, from, cond, guard)
103
159
  state_machine_name = @name
104
160
  #
105
161
  # Instead of one "may_event?" method that checks all variations every time it's called, here we check
106
162
  # 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)
163
+ method_lambda = if from == :any
164
+ from_any_may_event_lambda(guard, cond, state_machine_name)
113
165
  else
114
- guarded_may_event_lambda(from, guard, state_machine_name)
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
+
119
195
  def guarded_may_event_lambda(from, guard, state_machine_name)
120
196
  if from.is_a?(Array)
121
197
  lambda { # guarded transition from choice of states
@@ -130,6 +206,20 @@ module SimplyFSM
130
206
  end
131
207
  end
132
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
+
133
223
  def guardless_may_event_lambda(from, state_machine_name)
134
224
  if from.is_a?(Array)
135
225
  lambda { # unguarded transition from choice of states
@@ -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,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.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-28 00:00:00.000000000 Z
11
+ date: 2022-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -65,6 +65,7 @@ files:
65
65
  - spec/support/state_machine_examples.rb
66
66
  - spec/unit/fail_events_spec.rb
67
67
  - spec/unit/guard_events_spec.rb
68
+ - spec/unit/multi_transition_state_machine_spec.rb
68
69
  - spec/unit/one_state_machine_spec.rb
69
70
  - spec/unit/simply_fsm_spec.rb
70
71
  - spec/unit/two_state_machines_spec.rb
@@ -99,6 +100,7 @@ test_files:
99
100
  - spec/support/state_machine_examples.rb
100
101
  - spec/unit/fail_events_spec.rb
101
102
  - spec/unit/guard_events_spec.rb
103
+ - spec/unit/multi_transition_state_machine_spec.rb
102
104
  - spec/unit/one_state_machine_spec.rb
103
105
  - spec/unit/simply_fsm_spec.rb
104
106
  - spec/unit/two_state_machines_spec.rb