simply_fsm 0.2.3 → 0.3.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: 0ae5d7b10e3ba09f4838625a6de8dd2314b68246628efed8d950fe087f34f1d7
4
- data.tar.gz: cb2f1af32ed62f1032e902391543e6347d0c367eabbe1795e0caf067099baece
3
+ metadata.gz: e96963bd4f2d0cf605ca52b79cc74c3b3ba29d66df452683c6425f6e4243cf62
4
+ data.tar.gz: ca0ad99246f5f27eb9749ba2dd41c652bd424c55828cdeb4f6365478d8866488
5
5
  SHA512:
6
- metadata.gz: 7f8e0821322ec9e24b9eb5626d59c30e8fa5863d174c53c1d77d041351a635f0a690a111057c3bbd8d58e2aa8418384996314fb39e1f43db3c31c825f229045b
7
- data.tar.gz: f7fc248239ca69a84f54ac8a94e25e6bcfc7986e37c40c4bd2dbc5936d4e2a4168be5afbef66351c94f7aabc07debc27ab78ff8133426a6d140082a19f2f2138
6
+ metadata.gz: 37de0fd3446033ef390fb8aa6bf616c936de2125e65b876724904b989163d816f52971198a5fb2055a9412589565633a5fbaaa93eaed35912da549bc366d1446
7
+ data.tar.gz: 97165581f0a8f1955ffbb568d99958e84a99de955041ead58b52a5ed100eee52fd8cc58175f32702a11c0d4d9c86a62ff858cc306b3b3a0efd52ec5e3cfdcf02
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
- --format documentation
1
+ --format progress
2
2
  --color
3
3
  --require spec_helper
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  - None right now
4
4
 
5
+ ## [0.3.0]
6
+
7
+ - Moving main source under the `lib/simply_fsm` folder to remove `require` statement
8
+ - Why? Eventually want to be able to use this with `mruby`
9
+
5
10
  ## [0.2.3] - 2022-04-09
6
11
 
7
12
  - Add `rake yard` to generate local documentation
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simply_fsm (0.2.3)
4
+ simply_fsm (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Include *SimplyFSM* in a class to be able to defined state machines.
5
+ #
6
+ module SimplyFSM
7
+ #
8
+ # Provides a +state_machine+ for the including class.
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ #
14
+ # Defines the constructor for defining a state machine
15
+ module ClassMethods
16
+ #
17
+ # Declare a state machine called +name+ which can then be defined
18
+ # by a DSL defined by the methods of *StateMachine*.
19
+ #
20
+ # @param [String] name of the state machine.
21
+ # @param [Hash] opts to specify options such as:
22
+ # - +fail+ lambda that is called with the event name when any event fails to transition
23
+ #
24
+ def state_machine(name, opts = {}, &block)
25
+ fsm = StateMachine.new(name, self, fail: opts[:fail])
26
+ fsm.instance_eval(&block)
27
+ end
28
+ end
29
+
30
+ ##
31
+ # The DSL for defining a state machine. These methods are used within the declaration of a +state_machine+.
32
+ #
33
+ # @attr_reader [String] initial_state The initial state of the state machine
34
+ # @attr_reader [Array] states All the states of the state machine
35
+ # @attr_reader [Array] events All the events of the state machine
36
+ # @attr_reader [String] name
37
+ # @attr_reader [String] full_name The name of the owning class combined with the state machine's name
38
+ #
39
+ class StateMachine
40
+ attr_reader :initial_state, :states, :events, :name, :full_name
41
+
42
+ #
43
+ # @!visibility private
44
+ def initialize(name, owner_class, fail: nil)
45
+ @owner_class = owner_class
46
+ @name = name.to_sym
47
+ @full_name = "#{owner_class.name}/#{name}"
48
+ @states = []
49
+ @events = []
50
+ @initial_state = nil
51
+ @fail_handler = fail
52
+
53
+ setup_base_methods
54
+ end
55
+
56
+ #
57
+ # Declare a supported +state_name+, and optionally specify one as the +initial+ state.
58
+ #
59
+ # @param [String] state_name
60
+ # @param [Boolean] initial to indicate if this is the initial state of the state machine
61
+ #
62
+ def state(state_name, initial: false)
63
+ return if state_name.nil? || @states.include?(state_name)
64
+
65
+ status = state_name.to_sym
66
+ state_machine_name = @name
67
+ @states << status
68
+ @initial_state = status if initial
69
+
70
+ make_owner_method "#{state_name}?", lambda {
71
+ send(state_machine_name) == status
72
+ }
73
+ end
74
+
75
+ ##
76
+ # Define an event by +event_name+
77
+ #
78
+ # @param [String] event_name
79
+ # @param [Hash,Array] transitions either one (Hash) or many (Array of Hashes) transitions +from+ one state +to+ another state.
80
+ # @param [Lambda] guard if specified must return +true+ before any transitions are attempted
81
+ # @param [Lambda] fail called with event name if specified when all the attempted transitions fail
82
+ # @yield when the transition attempt succeeds.
83
+ def event(event_name, transitions:, guard: nil, fail: nil, &after)
84
+ return unless event_exists?(event_name) && transitions
85
+
86
+ @events << event_name
87
+ may_event_name = "may_#{event_name}?"
88
+
89
+ if transitions.is_a?(Array)
90
+ setup_multi_transition_may_event_method transitions: transitions, guard: guard,
91
+ may_event_name: may_event_name
92
+ setup_multi_transition_event_method event_name,
93
+ transitions: transitions, guard: guard,
94
+ var_name: "@#{@name}", fail: fail || @fail_handler
95
+ return
96
+ end
97
+
98
+ to = transitions[:to]
99
+ setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], guard
100
+ setup_event_method event_name, var_name: "@#{@name}",
101
+ may_event_name: may_event_name, to: to,
102
+ fail: fail || @fail_handler, &after
103
+ end
104
+
105
+ private
106
+
107
+ def setup_multi_transition_may_event_method(transitions:, guard:, may_event_name:)
108
+ state_machine_name = @name
109
+
110
+ make_owner_method may_event_name, lambda {
111
+ if !guard || instance_exec(&guard)
112
+ current = send(state_machine_name)
113
+ # Check each transition, and first one that succeeds ends the scan
114
+ transitions.each do |t|
115
+ next if cannot_transition?(t[:from], t[:when], current)
116
+
117
+ return true
118
+ end
119
+ end
120
+ false
121
+ }
122
+ end
123
+
124
+ def setup_fail_lambda_for(fail)
125
+ return unless fail
126
+
127
+ if fail.is_a?(String) || fail.is_a?(Symbol)
128
+ ->(event_name) { send(fail, event_name) }
129
+ else
130
+ fail
131
+ end
132
+ end
133
+
134
+ def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
135
+ state_machine_name = @name
136
+ fail_lambda = setup_fail_lambda_for(fail)
137
+ make_owner_method event_name, lambda {
138
+ if !guard || instance_exec(&guard)
139
+ current = send(state_machine_name)
140
+ # Check each transition, and first one that succeeds ends the scan
141
+ transitions.each do |t|
142
+ next if cannot_transition?(t[:from], t[:when], current)
143
+
144
+ instance_variable_set(var_name, t[:to])
145
+ return true
146
+ end
147
+ end
148
+ instance_exec(event_name, &fail_lambda) if fail_lambda
149
+ false
150
+ }
151
+ end
152
+
153
+ def event_exists?(event_name)
154
+ event_name && !@events.include?(event_name)
155
+ end
156
+
157
+ def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
158
+ fail_lambda = setup_fail_lambda_for(fail)
159
+ method_lambda = lambda {
160
+ if send(may_event_name)
161
+ instance_variable_set(var_name, to)
162
+ instance_exec(&after) if after
163
+ return true
164
+ end
165
+ instance_exec(event_name, &fail_lambda) if fail_lambda
166
+ false
167
+ }
168
+ make_owner_method event_name, method_lambda
169
+ end
170
+
171
+ def setup_may_event_method(may_event_name, from, cond, guard)
172
+ state_machine_name = @name
173
+ #
174
+ # Instead of one "may_event?" method that checks all variations every time it's called, here we check
175
+ # the event definition and define the most optimal lambda to ensure the check is as fast as possible
176
+ method_lambda = if from == :any
177
+ from_any_may_event_lambda(guard, cond, state_machine_name)
178
+ else
179
+ guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
180
+ end
181
+ make_owner_method may_event_name, method_lambda
182
+ end
183
+
184
+ def from_any_may_event_lambda(guard, cond, _state_machine_name)
185
+ if !guard && !cond
186
+ -> { true } # unguarded transition from any state
187
+ elsif !cond
188
+ guard # guarded transition from any state
189
+ elsif !guard
190
+ cond # conditional unguarded transition from any state
191
+ else
192
+ -> { instance_exec(&guard) && instance_exec(&cond) }
193
+ end
194
+ end
195
+
196
+ def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
197
+ if !guard && !cond
198
+ guardless_may_event_lambda(from, state_machine_name)
199
+ elsif !cond
200
+ guarded_may_event_lambda(from, guard, state_machine_name)
201
+ elsif !guard
202
+ guarded_may_event_lambda(from, cond, state_machine_name)
203
+ else
204
+ guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
205
+ end
206
+ end
207
+
208
+ def guarded_may_event_lambda(from, guard, state_machine_name)
209
+ if from.is_a?(Array)
210
+ lambda { # guarded transition from choice of states
211
+ current = send(state_machine_name)
212
+ from.include?(current) && instance_exec(&guard)
213
+ }
214
+ else
215
+ lambda { # guarded transition from one state
216
+ current = send(state_machine_name)
217
+ from == current && instance_exec(&guard)
218
+ }
219
+ end
220
+ end
221
+
222
+ def guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
223
+ if from.is_a?(Array)
224
+ lambda { # guarded transition from choice of states
225
+ current = send(state_machine_name)
226
+ from.include?(current) && instance_exec(&guard) && instance_exec(&cond)
227
+ }
228
+ else
229
+ lambda { # guarded transition from one state
230
+ current = send(state_machine_name)
231
+ from == current && instance_exec(&guard) && instance_exec(&cond)
232
+ }
233
+ end
234
+ end
235
+
236
+ def guardless_may_event_lambda(from, state_machine_name)
237
+ if from.is_a?(Array)
238
+ lambda { # unguarded transition from choice of states
239
+ current = send(state_machine_name)
240
+ from.include?(current)
241
+ }
242
+ else
243
+ lambda { # unguarded transition from one state
244
+ current = send(state_machine_name)
245
+ from == current
246
+ }
247
+ end
248
+ end
249
+
250
+ def setup_base_methods
251
+ var_name = "@#{name}"
252
+ fsm = self
253
+ make_owner_method @name, lambda {
254
+ instance_variable_get(var_name) ||
255
+ fsm.initial_state
256
+ }
257
+ make_owner_method "#{@name}_states", -> { fsm.states }
258
+ make_owner_method "#{@name}_events", -> { fsm.events }
259
+ end
260
+
261
+ def make_owner_method(method_name, method_definition)
262
+ @owner_class.define_method(method_name, method_definition)
263
+ end
264
+ end
265
+
266
+ private
267
+
268
+ def state_match?(from, current)
269
+ return true if from == :any
270
+ return from.include?(current) if from.is_a?(Array)
271
+
272
+ from == current
273
+ end
274
+
275
+ def cannot_transition?(from, cond, current)
276
+ (from && !state_match?(from, current)) || (cond && !instance_exec(&cond))
277
+ end
278
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimplyFSM
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/simply_fsm.rb CHANGED
@@ -1,280 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "simply_fsm/version"
4
-
5
- #
6
- # Include *SimplyFSM* in a class to be able to defined state machines.
7
- #
8
- module SimplyFSM
9
- #
10
- # Provides a +state_machine+ for the including class.
11
- def self.included(base)
12
- base.extend(ClassMethods)
13
- end
14
-
15
- #
16
- # Defines the constructor for defining a state machine
17
- module ClassMethods
18
- #
19
- # Declare a state machine called +name+ which can then be defined
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
- #
26
- def state_machine(name, opts = {}, &block)
27
- fsm = StateMachine.new(name, self, fail: opts[:fail])
28
- fsm.instance_eval(&block)
29
- end
30
- end
31
-
32
- ##
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
- #
41
- class StateMachine
42
- attr_reader :initial_state, :states, :events, :name, :full_name
43
-
44
- #
45
- # @!visibility private
46
- def initialize(name, owner_class, fail: nil)
47
- @owner_class = owner_class
48
- @name = name.to_sym
49
- @full_name = "#{owner_class.name}/#{name}"
50
- @states = []
51
- @events = []
52
- @initial_state = nil
53
- @fail_handler = fail
54
-
55
- setup_base_methods
56
- end
57
-
58
- #
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
- #
64
- def state(state_name, initial: false)
65
- return if state_name.nil? || @states.include?(state_name)
66
-
67
- status = state_name.to_sym
68
- state_machine_name = @name
69
- @states << status
70
- @initial_state = status if initial
71
-
72
- make_owner_method "#{state_name}?", lambda {
73
- send(state_machine_name) == status
74
- }
75
- end
76
-
77
- ##
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
87
-
88
- @events << event_name
89
- may_event_name = "may_#{event_name}?"
90
-
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
102
- setup_event_method event_name, var_name: "@#{@name}",
103
- may_event_name: may_event_name, to: to,
104
- fail: fail || @fail_handler, &after
105
- end
106
-
107
- private
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
-
155
- def event_exists?(event_name)
156
- event_name && !@events.include?(event_name)
157
- end
158
-
159
- def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after)
160
- fail_lambda = setup_fail_lambda_for(fail)
161
- method_lambda = lambda {
162
- if send(may_event_name)
163
- instance_variable_set(var_name, to)
164
- instance_exec(&after) if after
165
- return true
166
- end
167
- instance_exec(event_name, &fail_lambda) if fail_lambda
168
- false
169
- }
170
- make_owner_method event_name, method_lambda
171
- end
172
-
173
- def setup_may_event_method(may_event_name, from, cond, guard)
174
- state_machine_name = @name
175
- #
176
- # Instead of one "may_event?" method that checks all variations every time it's called, here we check
177
- # the event definition and define the most optimal lambda to ensure the check is as fast as possible
178
- method_lambda = if from == :any
179
- from_any_may_event_lambda(guard, cond, state_machine_name)
180
- else
181
- guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
182
- end
183
- make_owner_method may_event_name, method_lambda
184
- end
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
-
210
- def guarded_may_event_lambda(from, guard, state_machine_name)
211
- if from.is_a?(Array)
212
- lambda { # guarded transition from choice of states
213
- current = send(state_machine_name)
214
- from.include?(current) && instance_exec(&guard)
215
- }
216
- else
217
- lambda { # guarded transition from one state
218
- current = send(state_machine_name)
219
- from == current && instance_exec(&guard)
220
- }
221
- end
222
- end
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
-
238
- def guardless_may_event_lambda(from, state_machine_name)
239
- if from.is_a?(Array)
240
- lambda { # unguarded transition from choice of states
241
- current = send(state_machine_name)
242
- from.include?(current)
243
- }
244
- else
245
- lambda { # unguarded transition from one state
246
- current = send(state_machine_name)
247
- from == current
248
- }
249
- end
250
- end
251
-
252
- def setup_base_methods
253
- var_name = "@#{name}"
254
- fsm = self
255
- make_owner_method @name, lambda {
256
- instance_variable_get(var_name) ||
257
- fsm.initial_state
258
- }
259
- make_owner_method "#{@name}_states", -> { fsm.states }
260
- make_owner_method "#{@name}_events", -> { fsm.events }
261
- end
262
-
263
- def make_owner_method(method_name, method_definition)
264
- @owner_class.define_method(method_name, method_definition)
265
- end
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
280
- end
4
+ require "simply_fsm/simply_fsm"
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.2.3
4
+ version: 0.3.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-04-09 00:00:00.000000000 Z
11
+ date: 2022-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -74,6 +74,7 @@ files:
74
74
  - bin/console
75
75
  - bin/setup
76
76
  - lib/simply_fsm.rb
77
+ - lib/simply_fsm/simply_fsm.rb
77
78
  - lib/simply_fsm/version.rb
78
79
  - simply_fsm.gemspec
79
80
  - spec/spec_helper.rb