nxt_state_machine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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