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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +67 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +348 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/nxt_state_machine/callable.rb +63 -0
  14. data/lib/nxt_state_machine/callback_registry.rb +35 -0
  15. data/lib/nxt_state_machine/error_callback_registry.rb +38 -0
  16. data/lib/nxt_state_machine/errors/error.rb +1 -0
  17. data/lib/nxt_state_machine/errors/event_already_registered.rb +5 -0
  18. data/lib/nxt_state_machine/errors/event_without_transitions.rb +5 -0
  19. data/lib/nxt_state_machine/errors/initial_state_already_defined.rb +7 -0
  20. data/lib/nxt_state_machine/errors/invalid_callback_option.rb +5 -0
  21. data/lib/nxt_state_machine/errors/missing_configuration.rb +5 -0
  22. data/lib/nxt_state_machine/errors/state_already_registered.rb +5 -0
  23. data/lib/nxt_state_machine/errors/transition_already_registered.rb +5 -0
  24. data/lib/nxt_state_machine/errors/transition_halted.rb +12 -0
  25. data/lib/nxt_state_machine/errors/transition_not_defined.rb +5 -0
  26. data/lib/nxt_state_machine/errors/unknown_state_error.rb +5 -0
  27. data/lib/nxt_state_machine/event.rb +49 -0
  28. data/lib/nxt_state_machine/event_registry.rb +11 -0
  29. data/lib/nxt_state_machine/integrations/active_record.rb +77 -0
  30. data/lib/nxt_state_machine/integrations/attr_accessor.rb +69 -0
  31. data/lib/nxt_state_machine/integrations/hash.rb +67 -0
  32. data/lib/nxt_state_machine/state.rb +17 -0
  33. data/lib/nxt_state_machine/state_machine.rb +179 -0
  34. data/lib/nxt_state_machine/state_registry.rb +12 -0
  35. data/lib/nxt_state_machine/transition/around_callback_chain.rb +26 -0
  36. data/lib/nxt_state_machine/transition/proxy.rb +31 -0
  37. data/lib/nxt_state_machine/transition/store.rb +19 -0
  38. data/lib/nxt_state_machine/transition.rb +87 -0
  39. data/lib/nxt_state_machine/version.rb +3 -0
  40. data/lib/nxt_state_machine.rb +96 -0
  41. data/nxt_state_machine.gemspec +46 -0
  42. 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
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ .env.development.local
13
+ .env.test.local
14
+
15
+ !/**/.keep
16
+ /storage/
17
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.1
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.1
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in nxt_state_machine.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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)
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ EventAlreadyRegistered = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ EventWithoutTransitions = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ InitialStateAlreadyDefined = Class.new(Error)
4
+ end
5
+ end
6
+
7
+
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ InvalidCallbackOption = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ MissingConfiguration = Class.new(Error)
4
+ end
5
+ end