nxt_state_machine 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +348 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nxt_state_machine/callable.rb +63 -0
- data/lib/nxt_state_machine/callback_registry.rb +35 -0
- data/lib/nxt_state_machine/error_callback_registry.rb +38 -0
- data/lib/nxt_state_machine/errors/error.rb +1 -0
- data/lib/nxt_state_machine/errors/event_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/event_without_transitions.rb +5 -0
- data/lib/nxt_state_machine/errors/initial_state_already_defined.rb +7 -0
- data/lib/nxt_state_machine/errors/invalid_callback_option.rb +5 -0
- data/lib/nxt_state_machine/errors/missing_configuration.rb +5 -0
- data/lib/nxt_state_machine/errors/state_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/transition_already_registered.rb +5 -0
- data/lib/nxt_state_machine/errors/transition_halted.rb +12 -0
- data/lib/nxt_state_machine/errors/transition_not_defined.rb +5 -0
- data/lib/nxt_state_machine/errors/unknown_state_error.rb +5 -0
- data/lib/nxt_state_machine/event.rb +49 -0
- data/lib/nxt_state_machine/event_registry.rb +11 -0
- data/lib/nxt_state_machine/integrations/active_record.rb +77 -0
- data/lib/nxt_state_machine/integrations/attr_accessor.rb +69 -0
- data/lib/nxt_state_machine/integrations/hash.rb +67 -0
- data/lib/nxt_state_machine/state.rb +17 -0
- data/lib/nxt_state_machine/state_machine.rb +179 -0
- data/lib/nxt_state_machine/state_registry.rb +12 -0
- data/lib/nxt_state_machine/transition/around_callback_chain.rb +26 -0
- data/lib/nxt_state_machine/transition/proxy.rb +31 -0
- data/lib/nxt_state_machine/transition/store.rb +19 -0
- data/lib/nxt_state_machine/transition.rb +87 -0
- data/lib/nxt_state_machine/version.rb +3 -0
- data/lib/nxt_state_machine.rb +96 -0
- data/nxt_state_machine.gemspec +46 -0
- metadata +202 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a5b521a6ccdd3c6538bb1941f4cbccce8a7e83759a2f5d0c6417d2e479ee6a94
|
4
|
+
data.tar.gz: a79d3f8f70987524e2246650e646cef2978baf869ccd42b198e389033c0ff645
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2bc0e9e567a1cef8ba5c98e05d635491ec282f7b548a27172d9972d4ae57dbed753f93f9502560cf430405a7789cf29b12e8425a55f1dfc09e4cd79031e8957f
|
7
|
+
data.tar.gz: 6c0f560702961e7afa1025fb33a57ff57df0ab05ba543abef568eb7ca987dcd738586a25e06ae5b538f7b01f9b6043322c4bee19a8df9c9471f323206378e5c0
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
nxt_state_machine (0.1.0)
|
5
|
+
activesupport
|
6
|
+
nxt_registry (~> 0.1.3)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activemodel (6.0.0)
|
12
|
+
activesupport (= 6.0.0)
|
13
|
+
activerecord (6.0.0)
|
14
|
+
activemodel (= 6.0.0)
|
15
|
+
activesupport (= 6.0.0)
|
16
|
+
activesupport (6.0.0)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 0.7, < 2)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
zeitwerk (~> 2.1, >= 2.1.8)
|
22
|
+
coderay (1.1.2)
|
23
|
+
concurrent-ruby (1.1.5)
|
24
|
+
diff-lcs (1.3)
|
25
|
+
i18n (1.7.0)
|
26
|
+
concurrent-ruby (~> 1.0)
|
27
|
+
method_source (0.9.2)
|
28
|
+
minitest (5.12.2)
|
29
|
+
nxt_registry (0.1.3)
|
30
|
+
activesupport
|
31
|
+
pry (0.12.2)
|
32
|
+
coderay (~> 1.1.0)
|
33
|
+
method_source (~> 0.9.0)
|
34
|
+
rake (10.5.0)
|
35
|
+
rspec (3.9.0)
|
36
|
+
rspec-core (~> 3.9.0)
|
37
|
+
rspec-expectations (~> 3.9.0)
|
38
|
+
rspec-mocks (~> 3.9.0)
|
39
|
+
rspec-core (3.9.0)
|
40
|
+
rspec-support (~> 3.9.0)
|
41
|
+
rspec-expectations (3.9.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.9.0)
|
44
|
+
rspec-mocks (3.9.0)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.9.0)
|
47
|
+
rspec-support (3.9.0)
|
48
|
+
sqlite3 (1.4.1)
|
49
|
+
thread_safe (0.3.6)
|
50
|
+
tzinfo (1.2.5)
|
51
|
+
thread_safe (~> 0.1)
|
52
|
+
zeitwerk (2.2.0)
|
53
|
+
|
54
|
+
PLATFORMS
|
55
|
+
ruby
|
56
|
+
|
57
|
+
DEPENDENCIES
|
58
|
+
activerecord
|
59
|
+
bundler (~> 2.0)
|
60
|
+
nxt_state_machine!
|
61
|
+
pry
|
62
|
+
rake (~> 10.0)
|
63
|
+
rspec (~> 3.0)
|
64
|
+
sqlite3
|
65
|
+
|
66
|
+
BUNDLED WITH
|
67
|
+
2.0.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Andreas Robecke
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# NxtStateMachine
|
2
|
+
|
3
|
+
NxtStateMachine is a simple state machine library that ships with an easy to use integration for ActiveRecord.
|
4
|
+
It was build with the intend in mind to make it easy to implement other integrations.
|
5
|
+
Beside the ActiveRecord integration, it ships with in memory adapters for Hash and attr_accessor.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'nxt_state_machine'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install nxt_state_machine
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
### ActiveRecord Example
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class ArticleWorkflow
|
29
|
+
include NxtStateMachine::ActiveRecord
|
30
|
+
|
31
|
+
def initialize(article, **options)
|
32
|
+
@article = article
|
33
|
+
@options = options
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor :article
|
37
|
+
|
38
|
+
state_machine(target: :article, state_attr: :status) do
|
39
|
+
state :draft, initial: true
|
40
|
+
state :written
|
41
|
+
state :submitted
|
42
|
+
state :approved
|
43
|
+
state :published
|
44
|
+
state :rejected
|
45
|
+
state :deleted
|
46
|
+
|
47
|
+
event :write do
|
48
|
+
transition from: %i[draft written deleted], to: :written
|
49
|
+
end
|
50
|
+
|
51
|
+
event :submit do
|
52
|
+
# When the block takes arguments (instead of only keyword arguments!!)
|
53
|
+
# the transition is always passed in as the first argument!!!
|
54
|
+
transition from: %i[written rejected deleted], to: :submitted do |transition|
|
55
|
+
puts transition.from.enum
|
56
|
+
puts transition.to.enum
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
event :approve do
|
61
|
+
before_transition from: %i[written submitted deleted], to: :approved, run: :call_me_back
|
62
|
+
|
63
|
+
transition from: %i[written submitted deleted], to: :approved do |headline:|
|
64
|
+
article.headline = headline
|
65
|
+
end
|
66
|
+
|
67
|
+
after_transition from: %i[written submitted deleted], to: :approved, run: :call_me_back
|
68
|
+
|
69
|
+
around_transition from: any_state, to: :approved do |block|
|
70
|
+
# Note that around transition callbacks get passed a proc object that you have to call
|
71
|
+
puts 'around transition enter'
|
72
|
+
block.call
|
73
|
+
puts 'around transition exit'
|
74
|
+
end
|
75
|
+
|
76
|
+
on_error CustomError from: any_state, to: :approved do |error, transition|
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
event :publish do
|
81
|
+
before_transition from: any_state, to: :published, run: :some_method
|
82
|
+
|
83
|
+
transition from: :approved, to: :published
|
84
|
+
end
|
85
|
+
|
86
|
+
event :reject do
|
87
|
+
transition from: %i[draft submitted deleted], to: :rejected
|
88
|
+
end
|
89
|
+
|
90
|
+
event :delete do
|
91
|
+
transition from: any_state, to: :deleted do
|
92
|
+
article.deleted_at = Time.current
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
on_error! CustomError from: any_state, to: :approved do |error, transition|
|
97
|
+
# Would overwrite an existing error handler
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def some_method
|
104
|
+
end
|
105
|
+
|
106
|
+
def call_me_back(transition)
|
107
|
+
puts transition.from.enum
|
108
|
+
puts transition.to.enum
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
### ActiveRecord
|
114
|
+
|
115
|
+
In order to use nxt_state_machine with ActiveRecord simply `include NxtStateMachine::ActiveRecord` into your class.
|
116
|
+
This does not necessarily have to be a model (thus an instance of ActiveRecord) itself. If you are a fan of the single
|
117
|
+
responsibility principle you might want to put your workflow logic in a separate class instead of into the model directly.
|
118
|
+
Therefore simply define the target of your state machine as follows. This enables you to split up complex workflows into
|
119
|
+
multiple classes (maybe orchestrated by another toplevel workflow). If you do not provide a specific target, an instance
|
120
|
+
of the class you include nxt_state_machine into will be the target (most likely your model).
|
121
|
+
|
122
|
+
#### Define which object holds your state with the target: option
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Workflow
|
126
|
+
include NxtStateMachine::ActiveRecord
|
127
|
+
|
128
|
+
def initialize(article)
|
129
|
+
@article = article
|
130
|
+
end
|
131
|
+
|
132
|
+
attr_reader :article
|
133
|
+
|
134
|
+
state_machine(target: :article) do
|
135
|
+
# ...
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
#### Define which attribute holds your state with the state_attr: option
|
141
|
+
|
142
|
+
Customize which attribute is used to persist and fetch your state with `state_machine(state_attr: :state) do`.
|
143
|
+
If this is not customized, nxt_state_machine assumes your target has a `:state` attribute.
|
144
|
+
|
145
|
+
### States
|
146
|
+
|
147
|
+
The initial state will be set on new records that do not yet have a state set.
|
148
|
+
Of course there can only be one initial state.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class Article < ApplicationRecord
|
152
|
+
include NxtStateMachine::ActiveRecord
|
153
|
+
|
154
|
+
state_machine do
|
155
|
+
state :draft, initial: true
|
156
|
+
states :written, :submitted
|
157
|
+
# You can pass options to states that you can query in a transition later
|
158
|
+
state :deleted, end_state: true
|
159
|
+
|
160
|
+
# You can even define custom methods on states if options are not sufficient
|
161
|
+
state :advanced do
|
162
|
+
def advanced_state?
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
### Events
|
171
|
+
|
172
|
+
Once you have defined your states you can define events and their transitions. Events trigger state transitions based
|
173
|
+
on the current state of your target.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class Article < ApplicationRecord
|
177
|
+
include NxtStateMachine::ActiveRecord
|
178
|
+
|
179
|
+
state_machine do
|
180
|
+
state :draft, initial: true
|
181
|
+
states :written, :approved, :rejected, :published
|
182
|
+
|
183
|
+
event :write do
|
184
|
+
transition from: :draft, to: :written
|
185
|
+
transition from: :rejected, to: :written
|
186
|
+
# same as transition from: %i[draft rejected], to: :written
|
187
|
+
end
|
188
|
+
|
189
|
+
event :reject do
|
190
|
+
transition from: all_states, to: :rejected # all_states is equivalent to any_state
|
191
|
+
end
|
192
|
+
|
193
|
+
event :approve do
|
194
|
+
# We recommend to use keyword arguments to make events accept custom arguments
|
195
|
+
transition from: %i[written rejected], to: :approved do |approved_at:|
|
196
|
+
self.approved_at = approved_at
|
197
|
+
# NOTE: The transition is halted if this returns a falsey value
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
The events above define the following methods in your workflow class.
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
article.write
|
208
|
+
article.write!
|
209
|
+
# ...
|
210
|
+
# Generally speaking
|
211
|
+
article.<event_name> # will run the transition and call save on your target
|
212
|
+
article.<event_name!> # Will run the transition and call save! on your target
|
213
|
+
|
214
|
+
# Event that accepts keyword arguments
|
215
|
+
article.approve(approved_at: Time.current)
|
216
|
+
article.approve!(approved_at: Time.current)
|
217
|
+
```
|
218
|
+
|
219
|
+
**NOTE:** Transitions run in transactions that will be rolled back in case of an exception or if your target cannot be
|
220
|
+
saved due to validation errors. The state is then set back to the state before the transition!
|
221
|
+
|
222
|
+
### Transitions
|
223
|
+
|
224
|
+
When your transition takes arguments other than keyword arguments, it will always be passed the transition object itself
|
225
|
+
as the first argument. You can of course still accept keyword arguments. The transition object gives you access to the
|
226
|
+
state objects with `transition.from` and `transition.to`. Now you can query the options and methods you've defined
|
227
|
+
on those states earlier.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
event :approve do
|
231
|
+
transition from: %i[written rejected], to: :approved do |transition, approved_at:|
|
232
|
+
# The transition object provides some useful information in the current transition
|
233
|
+
puts transition.from # will give you the state object with the options and methods you defined earlier
|
234
|
+
puts transition.from.options # options hash
|
235
|
+
puts transition.to.enum # by calling :enum on the state it will give you the state enum
|
236
|
+
halt_transition if approved_at < 3.days.ago # This would halt the transition
|
237
|
+
"This is the return value if there is no error"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
```
|
241
|
+
|
242
|
+
#### Return values of transitions
|
243
|
+
|
244
|
+
Be aware that transitions that take blocks, return the return value of the block! This means that when your block returns
|
245
|
+
false, the transition would return false, even though the transition was executed just fine! (In that case is not equal
|
246
|
+
to tranistion did not succeed) If a transition does not take a block, it will return the value of `:save` and `:save!`
|
247
|
+
respectively.
|
248
|
+
|
249
|
+
#### Halting transitions
|
250
|
+
|
251
|
+
Transitions can be halted in callbacks and during the transition itself simply by calling `halt_transition`
|
252
|
+
|
253
|
+
### Callbacks
|
254
|
+
|
255
|
+
You can register `before_transition`, `around_transition` and `after_transition` callbacks. By defining the
|
256
|
+
:from and :to states you decide on which transitions the callback actually runs. Around callbacks need to call the
|
257
|
+
proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top level
|
258
|
+
behavious exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is
|
259
|
+
the :from and :to parameters with which they are registered.
|
260
|
+
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
event :approve do
|
264
|
+
before_transition from: %i[written submitted deleted], to: :approved, run: :call_me_back
|
265
|
+
|
266
|
+
transition from: %i[written submitted deleted], to: :approved
|
267
|
+
|
268
|
+
after_transition from: %i[written submitted deleted], to: :approved, run: :call_me_back
|
269
|
+
|
270
|
+
around_transition from: any_state, to: :approved do |block|
|
271
|
+
# Note that around transition callbacks get passed a proc object that you have to call
|
272
|
+
puts 'around transition enter'
|
273
|
+
block.call
|
274
|
+
puts 'around transition exit'
|
275
|
+
end
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
### Error Callbacks
|
280
|
+
|
281
|
+
You can also register callbacks that run in case of an error occurs. By defining the error class you can restrict
|
282
|
+
error callbacks to run on certain errors only. Error callbacks are applied in the order they are registered. You
|
283
|
+
can also overwrite previously registered errors with the bang method `on_error! CustomError ...`. By omitting the
|
284
|
+
error class a error callback is registered for all errors that inherit from `StandardError`.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
state_machine do
|
288
|
+
# ...
|
289
|
+
event :approve do
|
290
|
+
transition from: %i[written submitted deleted], to: :approved do |headline:|
|
291
|
+
article.headline = headline
|
292
|
+
end
|
293
|
+
|
294
|
+
on_error CustomError from: any_state, to: :approved do |error, transition|
|
295
|
+
# do something about the error here
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
on_error! CustomError from: any_state, to: :approved do |error, transition|
|
300
|
+
# overwrites previously registered error callbacks
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
### Multiple state machines in the same class
|
306
|
+
|
307
|
+
In theory you can also have multiple state_machines in the same class. To do so you have to give each
|
308
|
+
state_machine a name. Events need to be unique globally in order to determine which state_machine will be called.
|
309
|
+
You can also trigger events from one another.
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
class Article < ApplicationRecord
|
313
|
+
include NxtStateMachine::ActiveRecord
|
314
|
+
|
315
|
+
state_machine(:workflow) do
|
316
|
+
state :draft, initial: true
|
317
|
+
states :written, :approved, :rejected, :published
|
318
|
+
# ...
|
319
|
+
end
|
320
|
+
|
321
|
+
state_machine(:error_handling) do
|
322
|
+
# events need to be unique globally
|
323
|
+
end
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
|
328
|
+
## TODO
|
329
|
+
- Test implementations for Hash, AttrAccessor
|
330
|
+
- What about inheritance? => What would be the expected behaviour? (dup vs. no dup)
|
331
|
+
=> Might also make sense to walk the ancestors chain and collect configure blocks
|
332
|
+
=> This might be super flexible as we could apply these in amend / reset mode
|
333
|
+
=> Probably would be best to have :amend_configuration and :reset_configuration methods on the state_machine
|
334
|
+
|
335
|
+
|
336
|
+
## Development
|
337
|
+
|
338
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
339
|
+
|
340
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
341
|
+
|
342
|
+
## Contributing
|
343
|
+
|
344
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nxt_state_machine.
|
345
|
+
|
346
|
+
## License
|
347
|
+
|
348
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "nxt_state_machine"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Callable
|
3
|
+
def initialize(callee)
|
4
|
+
@callee = callee
|
5
|
+
|
6
|
+
if callee.is_a?(Symbol)
|
7
|
+
self.type = :method
|
8
|
+
elsif callee.respond_to?(:call)
|
9
|
+
self.type = :proc
|
10
|
+
self.context = callee.binding
|
11
|
+
else
|
12
|
+
raise ArgumentError, "Callee is nor symbol nor a proc: #{callee}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_context(execution_context = nil)
|
17
|
+
self.context = execution_context
|
18
|
+
ensure_context_not_missing
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(*args, **opts)
|
23
|
+
ensure_context_not_missing
|
24
|
+
|
25
|
+
args << opts
|
26
|
+
args = args.take(arity)
|
27
|
+
|
28
|
+
if method?
|
29
|
+
context.send(callee, *args)
|
30
|
+
else
|
31
|
+
context.instance_exec(*args, &callee)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def arity
|
36
|
+
if proc?
|
37
|
+
callee.arity
|
38
|
+
elsif method?
|
39
|
+
method = context.send(:method, callee)
|
40
|
+
method.arity
|
41
|
+
else
|
42
|
+
raise ArgumentError, "Can't resolve arity from #{callee}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def proc?
|
49
|
+
type == :proc
|
50
|
+
end
|
51
|
+
|
52
|
+
def method?
|
53
|
+
type == :method
|
54
|
+
end
|
55
|
+
|
56
|
+
def ensure_context_not_missing
|
57
|
+
return if context
|
58
|
+
raise ArgumentError, "Missing context: #{context}"
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_accessor :context, :callee, :type
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class CallbackRegistry
|
3
|
+
include ::NxtRegistry
|
4
|
+
|
5
|
+
def register(from, to, kind, method = nil, block = nil)
|
6
|
+
method_or_block = method || block
|
7
|
+
return unless method_or_block
|
8
|
+
|
9
|
+
Array(from).each do |from_state|
|
10
|
+
Array(to).each do |to_state|
|
11
|
+
callbacks.from(from_state).to(to_state).kind(kind) << method_or_block
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def resolve(transition, kind = nil)
|
17
|
+
all_callbacks = callbacks.from(transition.from.enum).to(transition.to.enum)
|
18
|
+
return all_callbacks unless kind
|
19
|
+
|
20
|
+
all_callbacks.kind(kind)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def callbacks
|
26
|
+
@callbacks ||= registry :from do
|
27
|
+
nested :to do
|
28
|
+
nested :kind, default: -> { [] } do
|
29
|
+
attrs :before, :after
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class ErrorCallbackRegistry
|
3
|
+
include ::NxtRegistry
|
4
|
+
|
5
|
+
def register(from, to, error, method = nil, block = nil)
|
6
|
+
method_or_block = method || block
|
7
|
+
return unless method_or_block
|
8
|
+
|
9
|
+
Array(from).each do |from_state|
|
10
|
+
Array(to).each do |to_state|
|
11
|
+
callbacks.from(from_state).to(to_state).error(error, method_or_block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def resolve(error, transition)
|
17
|
+
candidate = callbacks.from(
|
18
|
+
transition.from.enum
|
19
|
+
).to(
|
20
|
+
transition.to.enum
|
21
|
+
).error.keys.find { |kind_of_error| error.is_a?(kind_of_error) }
|
22
|
+
|
23
|
+
return unless candidate
|
24
|
+
|
25
|
+
callbacks.from(transition.from.enum).to(transition.to.enum).error(candidate)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def callbacks
|
31
|
+
@callbacks ||= registry :from do
|
32
|
+
nested :to do
|
33
|
+
nested :error, transform_keys: false, call: false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Error = Class.new(StandardError)
|