nxt_state_machine 0.1.4 → 0.1.9
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 +4 -4
- data/.circleci/config.yml +8 -2
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +32 -28
- data/README.md +66 -20
- data/Rakefile +1 -1
- data/lib/nxt_state_machine.rb +6 -5
- data/lib/nxt_state_machine/callback_registry.rb +5 -5
- data/lib/nxt_state_machine/defuse_registry.rb +26 -0
- data/lib/nxt_state_machine/error_callback_registry.rb +2 -2
- data/lib/nxt_state_machine/event.rb +9 -2
- data/lib/nxt_state_machine/graph.rb +76 -0
- data/lib/nxt_state_machine/integrations/active_record.rb +53 -26
- data/lib/nxt_state_machine/integrations/attr_accessor.rb +2 -2
- data/lib/nxt_state_machine/integrations/hash.rb +2 -2
- data/lib/nxt_state_machine/state_machine.rb +23 -6
- data/lib/nxt_state_machine/transition.rb +10 -5
- data/lib/nxt_state_machine/transition/around_callback_chain.rb +1 -1
- data/lib/nxt_state_machine/transition/factory.rb +3 -3
- data/lib/nxt_state_machine/version.rb +1 -1
- data/lib/railtie.rb +8 -0
- data/lib/tasks/draw_graph.rake +8 -0
- data/nxt_state_machine.gemspec +3 -2
- data/state_machine.png +0 -0
- metadata +26 -7
- data/lib/nxt_state_machine/transition/store.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6128e445380f2ed2b836b4c23ed58556efd2ff0728add08c2cbd45e97812e068
|
4
|
+
data.tar.gz: 05c8861e99afefec3c654366050b225d7a93c10ff984e508cd14d6345b1d0ff3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3038d32da3a7bfa5a49b4537e7c6f163259ddd0f3d2b48a05bfa838021350f648d75bfc493125529c9e1374dc34aa2c9abe94325790fe4ba82a09f01e2afdc8
|
7
|
+
data.tar.gz: 64178102f125d3b180f31b1a0bbebd766226b44351bbef906d907cc6339f2ff213c743c51b52238706827cfd597e410b828eb0824f8a4747f363c9b2b79a0ce9
|
data/.circleci/config.yml
CHANGED
@@ -16,11 +16,17 @@ jobs:
|
|
16
16
|
steps:
|
17
17
|
- checkout
|
18
18
|
|
19
|
+
- run:
|
20
|
+
name: Install apt dependencies
|
21
|
+
command: |
|
22
|
+
sudo apt update -q \
|
23
|
+
&& sudo apt upgrade -q \
|
24
|
+
&& sudo apt install -qq graphviz
|
19
25
|
# Download and cache dependencies
|
20
26
|
- restore_cache:
|
21
27
|
keys:
|
22
28
|
- v1-dependencies-{{ checksum "Gemfile.lock" }}
|
23
|
-
|
29
|
+
|
24
30
|
- run: gem install bundler --version $BUNDLER_VERSION
|
25
31
|
|
26
32
|
- run:
|
@@ -53,4 +59,4 @@ jobs:
|
|
53
59
|
path: /tmp/test-results
|
54
60
|
- store_artifacts:
|
55
61
|
path: /tmp/test-results
|
56
|
-
destination: test-results
|
62
|
+
destination: test-results
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# v0.1.9 2020-09-23
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- Allow to toggle locking of transitions for active record adapter per event or globally
|
6
|
+
|
7
|
+
[Compare v0.1.8...v0.1.9](https://github.com/nxt-insurance/nxt_state_machine/compare/v0.1.8...v0.1.9)
|
8
|
+
|
9
|
+
|
10
|
+
# v0.1.8 2020-0-05
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- [internal] Allow to draw state machine graph with rake task
|
15
|
+
|
16
|
+
[Compare v0.1.7...v0.1.8](https://github.com/nxt-insurance/nxt_state_machine/compare/v0.1.7...v0.1.8)
|
data/Gemfile.lock
CHANGED
@@ -1,57 +1,60 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
nxt_state_machine (0.1.
|
4
|
+
nxt_state_machine (0.1.9)
|
5
5
|
activesupport
|
6
|
-
nxt_registry (~> 0.
|
6
|
+
nxt_registry (~> 0.3.0)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
activemodel (6.0.
|
12
|
-
activesupport (= 6.0.
|
13
|
-
activerecord (6.0.
|
14
|
-
activemodel (= 6.0.
|
15
|
-
activesupport (= 6.0.
|
16
|
-
activesupport (6.0.
|
11
|
+
activemodel (6.0.3.3)
|
12
|
+
activesupport (= 6.0.3.3)
|
13
|
+
activerecord (6.0.3.3)
|
14
|
+
activemodel (= 6.0.3.3)
|
15
|
+
activesupport (= 6.0.3.3)
|
16
|
+
activesupport (6.0.3.3)
|
17
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
18
|
i18n (>= 0.7, < 2)
|
19
19
|
minitest (~> 5.1)
|
20
20
|
tzinfo (~> 1.1)
|
21
|
-
zeitwerk (~> 2.2)
|
22
|
-
coderay (1.1.
|
23
|
-
concurrent-ruby (1.1.
|
24
|
-
diff-lcs (1.
|
25
|
-
i18n (1.8.
|
21
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
22
|
+
coderay (1.1.3)
|
23
|
+
concurrent-ruby (1.1.7)
|
24
|
+
diff-lcs (1.4.4)
|
25
|
+
i18n (1.8.5)
|
26
26
|
concurrent-ruby (~> 1.0)
|
27
|
-
method_source (0.
|
28
|
-
minitest (5.14.
|
29
|
-
nxt_registry (0.
|
27
|
+
method_source (1.0.0)
|
28
|
+
minitest (5.14.2)
|
29
|
+
nxt_registry (0.3.2)
|
30
30
|
activesupport
|
31
|
-
pry (0.
|
32
|
-
coderay (~> 1.1
|
33
|
-
method_source (~>
|
34
|
-
rake (
|
31
|
+
pry (0.13.1)
|
32
|
+
coderay (~> 1.1)
|
33
|
+
method_source (~> 1.0)
|
34
|
+
rake (12.3.3)
|
35
|
+
rexml (3.2.4)
|
35
36
|
rspec (3.9.0)
|
36
37
|
rspec-core (~> 3.9.0)
|
37
38
|
rspec-expectations (~> 3.9.0)
|
38
39
|
rspec-mocks (~> 3.9.0)
|
39
|
-
rspec-core (3.9.
|
40
|
-
rspec-support (~> 3.9.
|
41
|
-
rspec-expectations (3.9.
|
40
|
+
rspec-core (3.9.2)
|
41
|
+
rspec-support (~> 3.9.3)
|
42
|
+
rspec-expectations (3.9.2)
|
42
43
|
diff-lcs (>= 1.2.0, < 2.0)
|
43
44
|
rspec-support (~> 3.9.0)
|
44
|
-
rspec-mocks (3.9.
|
45
|
+
rspec-mocks (3.9.1)
|
45
46
|
diff-lcs (>= 1.2.0, < 2.0)
|
46
47
|
rspec-support (~> 3.9.0)
|
47
|
-
rspec-support (3.9.
|
48
|
+
rspec-support (3.9.3)
|
48
49
|
rspec_junit_formatter (0.4.1)
|
49
50
|
rspec-core (>= 2, < 4, != 2.12.0)
|
51
|
+
ruby-graphviz (1.2.5)
|
52
|
+
rexml
|
50
53
|
sqlite3 (1.4.2)
|
51
54
|
thread_safe (0.3.6)
|
52
|
-
tzinfo (1.2.
|
55
|
+
tzinfo (1.2.7)
|
53
56
|
thread_safe (~> 0.1)
|
54
|
-
zeitwerk (2.
|
57
|
+
zeitwerk (2.4.0)
|
55
58
|
|
56
59
|
PLATFORMS
|
57
60
|
ruby
|
@@ -61,9 +64,10 @@ DEPENDENCIES
|
|
61
64
|
bundler (~> 2.0)
|
62
65
|
nxt_state_machine!
|
63
66
|
pry
|
64
|
-
rake (~>
|
67
|
+
rake (~> 12.0)
|
65
68
|
rspec (~> 3.0)
|
66
69
|
rspec_junit_formatter
|
70
|
+
ruby-graphviz
|
67
71
|
sqlite3
|
68
72
|
|
69
73
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -83,6 +83,10 @@ class ArticleWorkflow
|
|
83
83
|
puts 'around transition exit'
|
84
84
|
end
|
85
85
|
|
86
|
+
on_success from: any_state, to: :approved do |transition|
|
87
|
+
# This is the last callback in the chain - It runs outside of the active record transaction
|
88
|
+
end
|
89
|
+
|
86
90
|
on_error CustomError from: any_state, to: :approved do |error, transition|
|
87
91
|
end
|
88
92
|
end
|
@@ -237,12 +241,14 @@ article.approve(approved_at: Time.current)
|
|
237
241
|
article.approve!(approved_at: Time.current)
|
238
242
|
```
|
239
243
|
|
240
|
-
**NOTE:** Transitions run in transactions that acquire a lock to prevent concurrency issues
|
241
|
-
rolled back in case of an exception or if your target cannot be saved due to validation errors.
|
242
|
-
The state is set back to the state before the transition! If you try to
|
244
|
+
**NOTE:** Transitions run in transactions that acquire a lock to prevent concurrency issues per default.
|
245
|
+
Transactions will be rolled back in case of an exception or if your target cannot be saved due to validation errors.
|
246
|
+
The state is set back to the state before the transition! If you try to transition on records with unpersisted changes
|
243
247
|
you will get a `RuntimeError: Locking a record with unpersisted changes is not supported.` error saying something
|
244
248
|
like `Use :save to persist the changes, or :reload to discard them explicitly.` since it's not possible to acquire a
|
245
|
-
lock on modified records.
|
249
|
+
lock on modified records. You can also switch of locking and transactions for events by passing in the `lock_transitions: false`
|
250
|
+
option when defining an event or globally on the state machine with the `lock_transitions: false` option. Currently
|
251
|
+
there is no option to toggle locking at runtime.
|
246
252
|
|
247
253
|
### Transitions
|
248
254
|
|
@@ -277,10 +283,10 @@ Transitions can be halted in callbacks and during the transition itself simply b
|
|
277
283
|
|
278
284
|
### Callbacks
|
279
285
|
|
280
|
-
You can register `before_transition`, `around_transition` and `
|
281
|
-
:from and :to states you decide on which transitions the callback actually runs. Around callbacks need
|
282
|
-
proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top
|
283
|
-
behaves exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is
|
286
|
+
You can register `before_transition`, `around_transition`, `after_transition` and `on_success` callbacks.
|
287
|
+
By defining the :from and :to states you decide on which transitions the callback actually runs. Around callbacks need
|
288
|
+
to call the proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top
|
289
|
+
level behaves exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is
|
284
290
|
the :from and :to parameters with which they are registered.
|
285
291
|
|
286
292
|
|
@@ -298,6 +304,11 @@ event :approve do
|
|
298
304
|
block.call
|
299
305
|
puts 'around transition exit'
|
300
306
|
end
|
307
|
+
|
308
|
+
# Use this to trigger another event after the transaction around the transition completed
|
309
|
+
on_success from: any_state, to: :approved do |transition|
|
310
|
+
# This is the last callback in the chain - It runs outside of the active record transaction
|
311
|
+
end
|
301
312
|
end
|
302
313
|
```
|
303
314
|
|
@@ -327,7 +338,53 @@ state_machine do
|
|
327
338
|
end
|
328
339
|
```
|
329
340
|
|
330
|
-
###
|
341
|
+
### ActiveRecord transaction, rollback and locks - breaking the flow by defusing errors
|
342
|
+
|
343
|
+
You want to break out of your transition (which is wrapped inside a lock)?
|
344
|
+
You can raise an error, have everything rolled back and then have your error handler take over.
|
345
|
+
**NOTE:** Unless you reload your model all assignments you did, previous to the error, should still be available in your
|
346
|
+
error handler. You can also defuse errors. This means they will not cause a rollback of the transaction during the
|
347
|
+
transition and you can actually persist changes to your model before the defused error is raised and handled. You can
|
348
|
+
also switch off locking (and transactions) for events by passing the `lock_transitions: false` option when defining an event. This
|
349
|
+
can also by set globally for a state_machine by passing the `lock_transitions: false` option when setting up the state
|
350
|
+
machine.
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
state_machine do
|
354
|
+
# ...
|
355
|
+
#
|
356
|
+
defuse CustomError, from: any_state, to: all_states
|
357
|
+
|
358
|
+
event :approve do
|
359
|
+
# You can also defuse on event level
|
360
|
+
# defuse CustomError, from: %i[written submitted deleted], to: :approved
|
361
|
+
|
362
|
+
transition from: %i[written submitted deleted], to: :approved do |headline:|
|
363
|
+
# This will be save to the database even if defused CustomError is raised after
|
364
|
+
article.update!(headline: headline)
|
365
|
+
raise CustomError, 'This does not rollback the headline update above'
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
event :approve_without_lock, lock_transitions: false do
|
370
|
+
transition from: %i[written submitted deleted], to: :approved do |headline:|
|
371
|
+
# This will be saved to the database because the event does not wrap the transition in a transaction
|
372
|
+
article.update!(headline: headline)
|
373
|
+
raise StandardError, 'This does not rollback the headline update above'
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
on_error! CustomError from: any_state, to: :approved do |error, transition|
|
378
|
+
# You can still handle the defused Error if you want to
|
379
|
+
# You should probably reload your model here to not accidentally save changes that
|
380
|
+
# were made to the model during the transition before a non defused error was raised
|
381
|
+
article.reload
|
382
|
+
# The error callback does not run inside the transaction. No more strings attached here.
|
383
|
+
# You can now persist changes to your model again.
|
384
|
+
article.update!(error: error.message)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
```
|
331
388
|
|
332
389
|
In theory you can also have multiple state_machines in the same class. To do so you have to give each
|
333
390
|
state_machine a name. Events need to be unique globally in order to determine which state_machine will be called.
|
@@ -349,17 +406,6 @@ class Article < ApplicationRecord
|
|
349
406
|
end
|
350
407
|
```
|
351
408
|
|
352
|
-
|
353
|
-
## TODO
|
354
|
-
- Test implementations for Hash, AttrAccessor
|
355
|
-
- Thread safety spec!
|
356
|
-
- Spec locks?
|
357
|
-
- Explain locking in readme!
|
358
|
-
- Should we clone machines for each context?
|
359
|
-
- What about inheritance? => What would be the expected behaviour? (dup vs. no dup)
|
360
|
-
=> Might also make sense to walk the ancestors chain and collect configure blocks
|
361
|
-
|
362
|
-
|
363
409
|
## Development
|
364
410
|
|
365
411
|
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.
|
data/Rakefile
CHANGED
data/lib/nxt_state_machine.rb
CHANGED
@@ -16,6 +16,7 @@ require "nxt_state_machine/callable"
|
|
16
16
|
require "nxt_state_machine/state_registry"
|
17
17
|
require "nxt_state_machine/callback_registry"
|
18
18
|
require "nxt_state_machine/error_callback_registry"
|
19
|
+
require "nxt_state_machine/defuse_registry"
|
19
20
|
require "nxt_state_machine/event_registry"
|
20
21
|
require "nxt_state_machine/state"
|
21
22
|
require "nxt_state_machine/event"
|
@@ -24,19 +25,19 @@ require "nxt_state_machine/transition/interface"
|
|
24
25
|
require "nxt_state_machine/transition"
|
25
26
|
require "nxt_state_machine/transition/factory"
|
26
27
|
require "nxt_state_machine/transition/proxy"
|
27
|
-
require "nxt_state_machine/transition/store"
|
28
28
|
require "nxt_state_machine/transition/around_callback_chain"
|
29
29
|
require "nxt_state_machine/state_machine"
|
30
30
|
require "nxt_state_machine/integrations/active_record"
|
31
31
|
require "nxt_state_machine/integrations/attr_accessor"
|
32
32
|
require "nxt_state_machine/integrations/hash"
|
33
|
+
require "nxt_state_machine/graph"
|
33
34
|
|
34
35
|
module NxtStateMachine
|
35
36
|
module ClassMethods
|
36
37
|
include NxtRegistry
|
37
38
|
|
38
39
|
def state_machine(name = :default, **opts, &config)
|
39
|
-
state_machines.resolve
|
40
|
+
state_machines.resolve(name) || state_machines.register(
|
40
41
|
name,
|
41
42
|
StateMachine.new(name, self, state_machine_event_registry, **opts).configure(&config)
|
42
43
|
)
|
@@ -74,15 +75,15 @@ module NxtStateMachine
|
|
74
75
|
end
|
75
76
|
|
76
77
|
def state_machine(name = :default)
|
77
|
-
@state_machine ||= self.class.state_machines.resolve(name)
|
78
|
+
@state_machine ||= self.class.state_machines.resolve!(name)
|
78
79
|
end
|
79
80
|
|
80
81
|
def current_state_name(name = :default)
|
81
|
-
state_machines.resolve(name).current_state_name(self)
|
82
|
+
state_machines.resolve!(name).current_state_name(self)
|
82
83
|
end
|
83
84
|
|
84
85
|
def current_state(name = :default)
|
85
|
-
state_machines.resolve(name).states.resolve(current_state_name(name))
|
86
|
+
state_machines.resolve!(name).states.resolve!(current_state_name(name))
|
86
87
|
end
|
87
88
|
|
88
89
|
def halt_transition(*args, **opts)
|
@@ -8,13 +8,13 @@ module NxtStateMachine
|
|
8
8
|
|
9
9
|
Array(from).each do |from_state|
|
10
10
|
Array(to).each do |to_state|
|
11
|
-
callbacks.from(from_state).to(to_state).kind(kind) << method_or_block
|
11
|
+
callbacks.from!(from_state).to!(to_state).kind!(kind) << method_or_block
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def resolve(transition, kind = nil)
|
17
|
-
all_callbacks = callbacks.from(transition.from.enum).to(transition.to.enum)
|
16
|
+
def resolve!(transition, kind = nil)
|
17
|
+
all_callbacks = callbacks.from!(transition.from.enum).to!(transition.to.enum)
|
18
18
|
return all_callbacks unless kind
|
19
19
|
|
20
20
|
all_callbacks.kind(kind)
|
@@ -24,8 +24,8 @@ module NxtStateMachine
|
|
24
24
|
|
25
25
|
def callbacks
|
26
26
|
@callbacks ||= registry :from do
|
27
|
-
|
28
|
-
|
27
|
+
level :to do
|
28
|
+
level :kind, default: -> { [] } do
|
29
29
|
attrs :before, :after
|
30
30
|
end
|
31
31
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class DefuseRegistry
|
3
|
+
include ::NxtRegistry
|
4
|
+
|
5
|
+
def register(from, to, kind)
|
6
|
+
Array(from).each do |from_state|
|
7
|
+
Array(to).each do |to_state|
|
8
|
+
defusing_errors = errors.from(from_state).to(to_state)
|
9
|
+
Array(kind).each_with_object(defusing_errors) { |error, acc| acc << error }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve!(transition)
|
15
|
+
errors.from!(transition.from.enum).to!(transition.to.enum)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def errors
|
21
|
+
@errors ||= registry :from do
|
22
|
+
level :to, default: -> { [] }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -7,22 +7,25 @@ module NxtStateMachine
|
|
7
7
|
@name = name
|
8
8
|
@event_transitions = registry("#{name} event transitions")
|
9
9
|
@names = Event::Names.build(name)
|
10
|
+
@options = options.with_indifferent_access
|
10
11
|
|
11
12
|
configure(&block)
|
12
13
|
|
13
14
|
ensure_event_has_transitions
|
14
15
|
end
|
15
16
|
|
16
|
-
attr_reader :name, :state_machine, :event_transitions, :names
|
17
|
+
attr_reader :name, :state_machine, :event_transitions, :names, :options
|
17
18
|
|
18
19
|
delegate :before_transition,
|
19
20
|
:after_transition,
|
20
21
|
:around_transition,
|
22
|
+
:on_success,
|
21
23
|
:on_error,
|
22
24
|
:on_error!,
|
23
25
|
:any_state,
|
24
26
|
:all_states,
|
25
27
|
:all_states_except,
|
28
|
+
:defuse,
|
26
29
|
to: :state_machine
|
27
30
|
|
28
31
|
def transitions(from:, to:, &block)
|
@@ -36,7 +39,11 @@ module NxtStateMachine
|
|
36
39
|
alias_method :transition, :transitions
|
37
40
|
|
38
41
|
def transitions_from?(state)
|
39
|
-
event_transitions.resolve
|
42
|
+
event_transitions.resolve(state).present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"#{self.class.name}[:#{name}]"
|
40
47
|
end
|
41
48
|
|
42
49
|
private
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Graph
|
3
|
+
def initialize(state_machines, **options)
|
4
|
+
@state_machines = state_machines
|
5
|
+
@options = default_options.merge(**options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def draw
|
9
|
+
require 'ruby-graphviz'
|
10
|
+
|
11
|
+
state_machines.each do |_, state_machine|
|
12
|
+
add_nodes(state_machine)
|
13
|
+
add_edges(state_machine)
|
14
|
+
end
|
15
|
+
|
16
|
+
filename = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
17
|
+
|
18
|
+
graph.output options[:format] => filename
|
19
|
+
|
20
|
+
puts '----------------------------------------------'
|
21
|
+
puts 'Please run the following to open the generated file:'
|
22
|
+
puts "open '#{filename}'"
|
23
|
+
puts '----------------------------------------------'
|
24
|
+
|
25
|
+
graph
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :options, :state_machines
|
31
|
+
|
32
|
+
def graph
|
33
|
+
@graph ||= ::GraphViz.new(
|
34
|
+
'G',
|
35
|
+
rankdir: options[:orientation] == 'landscape' ? 'LR' : 'TB',
|
36
|
+
ratio: options[:ratio]
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_nodes(state_machine)
|
41
|
+
state_machine.states.values.each do |state|
|
42
|
+
add_node(state)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_node(state)
|
47
|
+
node_options = {
|
48
|
+
label: state.to_s,
|
49
|
+
width: '1',
|
50
|
+
height: '1',
|
51
|
+
shape: 'ellipse'
|
52
|
+
}
|
53
|
+
|
54
|
+
graph.add_nodes(state.to_s, node_options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_edges(state_machine)
|
58
|
+
state_machine.events.values.each do |event|
|
59
|
+
event.event_transitions.values.each do |transition|
|
60
|
+
graph.add_edges(transition.from.to_s, transition.to.to_s, label: event.name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_options
|
66
|
+
{
|
67
|
+
name: 'state_machine',
|
68
|
+
path: '.',
|
69
|
+
orientation: 'landscape',
|
70
|
+
ratio: 'fill',
|
71
|
+
format: 'png',
|
72
|
+
font: 'Helvetica'
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module NxtStateMachine
|
2
2
|
module ActiveRecord
|
3
3
|
module ClassMethods
|
4
|
-
def state_machine(name = :default, state_attr: :state, target: nil, &config)
|
4
|
+
def state_machine(name = :default, state_attr: :state, target: nil, lock_transitions: true, &config)
|
5
5
|
machine = super(
|
6
6
|
name,
|
7
7
|
state_attr: state_attr,
|
8
8
|
target: target,
|
9
|
+
lock_transitions: lock_transitions,
|
9
10
|
&config
|
10
11
|
)
|
11
12
|
|
@@ -21,34 +22,21 @@ module NxtStateMachine
|
|
21
22
|
end
|
22
23
|
|
23
24
|
machine.set_state_with do |target, transition|
|
24
|
-
target
|
25
|
-
transition.run_before_callbacks
|
26
|
-
result = set_state(target, transition, state_attr, :save)
|
27
|
-
transition.run_after_callbacks
|
28
|
-
|
29
|
-
result
|
30
|
-
end
|
31
|
-
rescue StandardError => error
|
32
|
-
target.assign_attributes(state_attr => transition.from.to_s)
|
33
|
-
|
34
|
-
if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
35
|
-
false
|
36
|
-
else
|
37
|
-
raise
|
38
|
-
end
|
25
|
+
set_state(machine, target, transition, state_attr, :save)
|
39
26
|
end
|
40
27
|
|
41
28
|
machine.set_state_with! do |target, transition|
|
42
|
-
target
|
43
|
-
|
44
|
-
result = set_state(target, transition, state_attr, :save!)
|
45
|
-
transition.run_after_callbacks
|
29
|
+
set_state(machine, target, transition, state_attr, :save!)
|
30
|
+
end
|
46
31
|
|
47
|
-
|
32
|
+
machine.define_singleton_method :add_state_methods_to_model do |model_class|
|
33
|
+
model_class.class_eval do
|
34
|
+
machine.states.keys.each do |state_name|
|
35
|
+
define_method "#{state_name}?" do
|
36
|
+
send(machine.options.fetch(:state_attr)) == state_name
|
37
|
+
end
|
38
|
+
end
|
48
39
|
end
|
49
|
-
rescue StandardError
|
50
|
-
target.assign_attributes(state_attr => transition.from.to_s)
|
51
|
-
raise
|
52
40
|
end
|
53
41
|
|
54
42
|
machine
|
@@ -58,14 +46,53 @@ module NxtStateMachine
|
|
58
46
|
module InstanceMethods
|
59
47
|
private
|
60
48
|
|
61
|
-
def set_state(target, transition, state_attr,
|
49
|
+
def set_state(machine, target, transition, state_attr, save_with_method)
|
50
|
+
result = nil
|
51
|
+
defused_error = nil
|
52
|
+
|
53
|
+
with_conditional_lock(target, transition.event) do
|
54
|
+
transition.run_before_callbacks
|
55
|
+
result = execute_transition(target, transition, state_attr, save_with_method)
|
56
|
+
transition.run_after_callbacks
|
57
|
+
|
58
|
+
result
|
59
|
+
rescue StandardError => error
|
60
|
+
if machine.defuse_registry.resolve!(transition).find { |error_class| error.is_a?(error_class) }
|
61
|
+
defused_error = error
|
62
|
+
else
|
63
|
+
raise error
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
raise defused_error if defused_error
|
68
|
+
|
69
|
+
transition.run_success_callbacks || result
|
70
|
+
rescue StandardError => error
|
71
|
+
target.assign_attributes(state_attr => transition.from.to_s)
|
72
|
+
|
73
|
+
raise unless save_with_method == :save && error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
74
|
+
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute_transition(target, transition, state_attr, save_with_method)
|
62
79
|
transition.execute do |block|
|
63
80
|
result = block ? block.call : nil
|
64
81
|
target.assign_attributes(state_attr => transition.to.to_s)
|
65
|
-
set_state_result = target.send(
|
82
|
+
set_state_result = target.send(save_with_method) || halt_transition
|
66
83
|
block ? result : set_state_result
|
67
84
|
end
|
68
85
|
end
|
86
|
+
|
87
|
+
def with_conditional_lock(target, event, &block)
|
88
|
+
return block.call unless lock_transition?(event)
|
89
|
+
|
90
|
+
target.with_lock { block.call }
|
91
|
+
end
|
92
|
+
|
93
|
+
def lock_transition?(event)
|
94
|
+
event.options.fetch(:lock_transitions) { state_machine.options.fetch(:lock_transitions) }
|
95
|
+
end
|
69
96
|
end
|
70
97
|
|
71
98
|
def self.included(base)
|
@@ -22,7 +22,7 @@ module NxtStateMachine
|
|
22
22
|
result = set_state(target, transition, state_attr)
|
23
23
|
transition.run_after_callbacks
|
24
24
|
|
25
|
-
result
|
25
|
+
transition.run_success_callbacks || result
|
26
26
|
rescue StandardError => error
|
27
27
|
target.send("#{state_attr}=", transition.from.enum)
|
28
28
|
|
@@ -38,7 +38,7 @@ module NxtStateMachine
|
|
38
38
|
result = set_state(target, transition, state_attr)
|
39
39
|
transition.run_after_callbacks
|
40
40
|
|
41
|
-
result
|
41
|
+
transition.run_success_callbacks || result
|
42
42
|
rescue StandardError
|
43
43
|
target.send("#{state_attr}=", transition.from.enum)
|
44
44
|
raise
|
@@ -21,7 +21,7 @@ module NxtStateMachine
|
|
21
21
|
transition.run_before_callbacks
|
22
22
|
result = set_state(current_target, transition, state_attr)
|
23
23
|
transition.run_after_callbacks
|
24
|
-
result
|
24
|
+
transition.run_success_callbacks || result
|
25
25
|
rescue StandardError => error
|
26
26
|
current_target[state_attr] = transition.from.enum
|
27
27
|
|
@@ -37,7 +37,7 @@ module NxtStateMachine
|
|
37
37
|
result = set_state(current_target, transition, state_attr)
|
38
38
|
transition.run_after_callbacks
|
39
39
|
|
40
|
-
result
|
40
|
+
transition.run_success_callbacks || result
|
41
41
|
rescue StandardError
|
42
42
|
current_target[state_attr] = transition.from.enum
|
43
43
|
raise
|
@@ -6,15 +6,16 @@ module NxtStateMachine
|
|
6
6
|
@options = opts
|
7
7
|
|
8
8
|
@states = NxtStateMachine::StateRegistry.new
|
9
|
-
@transitions =
|
9
|
+
@transitions = []
|
10
10
|
@events = event_registry
|
11
11
|
@callbacks = CallbackRegistry.new
|
12
12
|
@error_callback_registry = ErrorCallbackRegistry.new
|
13
|
+
@defuse_registry = DefuseRegistry.new
|
13
14
|
|
14
15
|
@initial_state = nil
|
15
16
|
end
|
16
17
|
|
17
|
-
attr_reader :class_context, :
|
18
|
+
attr_reader :class_context, :events, :options, :callbacks, :name, :error_callback_registry, :defuse_registry
|
18
19
|
attr_accessor :initial_state
|
19
20
|
|
20
21
|
def get_state_with(method = nil, &block)
|
@@ -88,8 +89,9 @@ module NxtStateMachine
|
|
88
89
|
Event::Names.set_state_method_map(name).each do |event_name, set_state_method|
|
89
90
|
class_context.define_method event_name do |*args, **opts|
|
90
91
|
event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
|
91
|
-
transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
|
92
|
-
|
92
|
+
transition = event.event_transitions.resolve!(event.state_machine.current_state_name(self))
|
93
|
+
# Transition is build every time and thus should be thread safe!
|
94
|
+
transition.build_transition(event, self, set_state_method, *args, **opts)
|
93
95
|
end
|
94
96
|
end
|
95
97
|
|
@@ -99,7 +101,7 @@ module NxtStateMachine
|
|
99
101
|
end
|
100
102
|
|
101
103
|
def can_transition?(event_name, from)
|
102
|
-
event = events.resolve(event_name)
|
104
|
+
event = events.resolve!(event_name)
|
103
105
|
event && event.event_transitions.key?(from)
|
104
106
|
end
|
105
107
|
|
@@ -116,6 +118,14 @@ module NxtStateMachine
|
|
116
118
|
callbacks.register(from, to, :after, run, block)
|
117
119
|
end
|
118
120
|
|
121
|
+
def on_success(from:, to:, run: nil, &block)
|
122
|
+
callbacks.register(from, to, :success, run, block)
|
123
|
+
end
|
124
|
+
|
125
|
+
def defuse(errors = [], from:, to:)
|
126
|
+
defuse_registry.register(from, to, errors)
|
127
|
+
end
|
128
|
+
|
119
129
|
def on_error(error = StandardError, from:, to:, run: nil, &block)
|
120
130
|
error_callback_registry.register(from, to, error, run, block)
|
121
131
|
end
|
@@ -142,16 +152,23 @@ module NxtStateMachine
|
|
142
152
|
run_callbacks(transition, :after, context)
|
143
153
|
end
|
144
154
|
|
155
|
+
def run_success_callbacks(transition, context)
|
156
|
+
run_callbacks(transition, :success, context)
|
157
|
+
end
|
158
|
+
|
145
159
|
def find_error_callback(error, transition)
|
146
160
|
error_callback_registry.resolve(error, transition)
|
147
161
|
end
|
148
162
|
|
149
163
|
def run_callbacks(transition, kind, context)
|
150
|
-
current_callbacks = callbacks.resolve(transition, kind)
|
164
|
+
current_callbacks = callbacks.resolve!(transition, kind)
|
165
|
+
return unless current_callbacks.any?
|
151
166
|
|
152
167
|
current_callbacks.each do |callback|
|
153
168
|
Callable.new(callback).bind(context).call(transition)
|
154
169
|
end
|
170
|
+
|
171
|
+
true
|
155
172
|
end
|
156
173
|
|
157
174
|
def current_state_name(context)
|
@@ -5,15 +5,16 @@ module NxtStateMachine
|
|
5
5
|
def initialize(name, event:, from:, to:, state_machine:, context:, set_state_method:, &block)
|
6
6
|
@name = name
|
7
7
|
@event = event
|
8
|
-
@from = state_machine.states.resolve(from)
|
9
|
-
@to = state_machine.states.resolve(to)
|
8
|
+
@from = state_machine.states.resolve!(from)
|
9
|
+
@to = state_machine.states.resolve!(to)
|
10
10
|
@state_machine = state_machine
|
11
11
|
@set_state_method = set_state_method
|
12
12
|
@context = context
|
13
13
|
@block = block
|
14
|
+
@result = nil
|
14
15
|
end
|
15
16
|
|
16
|
-
attr_reader :name, :from, :to, :block, :event
|
17
|
+
attr_reader :name, :from, :to, :block, :event, :result
|
17
18
|
|
18
19
|
# This triggers the set state method
|
19
20
|
def trigger
|
@@ -31,7 +32,7 @@ module NxtStateMachine
|
|
31
32
|
|
32
33
|
# This must be used in set_state method to actually execute the transition within the around callback chain
|
33
34
|
def execute(&block)
|
34
|
-
Transition::Proxy.new(event, state_machine,self, context).call(&block)
|
35
|
+
self.result = Transition::Proxy.new(event, state_machine,self, context).call(&block)
|
35
36
|
end
|
36
37
|
|
37
38
|
alias_method :with_around_callbacks, :execute
|
@@ -44,9 +45,13 @@ module NxtStateMachine
|
|
44
45
|
state_machine.run_after_callbacks(self, context)
|
45
46
|
end
|
46
47
|
|
48
|
+
def run_success_callbacks
|
49
|
+
state_machine.run_success_callbacks(self, context)
|
50
|
+
end
|
51
|
+
|
47
52
|
private
|
48
53
|
|
49
54
|
attr_reader :state_machine, :set_state_method, :context
|
50
|
-
attr_writer :block
|
55
|
+
attr_writer :block, :result
|
51
56
|
end
|
52
57
|
end
|
@@ -18,7 +18,7 @@ module NxtStateMachine
|
|
18
18
|
private
|
19
19
|
|
20
20
|
def callbacks
|
21
|
-
@callbacks ||= state_machine.callbacks.resolve(transition).kind(:around)
|
21
|
+
@callbacks ||= state_machine.callbacks.resolve!(transition).kind(:around)
|
22
22
|
end
|
23
23
|
|
24
24
|
attr_reader :transition, :context, :state_machine
|
@@ -4,8 +4,8 @@ module NxtStateMachine
|
|
4
4
|
|
5
5
|
def initialize(name, from:, to:, state_machine:, &block)
|
6
6
|
@name = name
|
7
|
-
@from = state_machine.states.resolve(from)
|
8
|
-
@to = state_machine.states.resolve(to)
|
7
|
+
@from = state_machine.states.resolve!(from)
|
8
|
+
@to = state_machine.states.resolve!(to)
|
9
9
|
@state_machine = state_machine
|
10
10
|
@block = block
|
11
11
|
|
@@ -28,7 +28,7 @@ module NxtStateMachine
|
|
28
28
|
set_state_method: set_state_method
|
29
29
|
}
|
30
30
|
|
31
|
-
transition = Transition.new(name, **options)
|
31
|
+
transition = Transition.new(event.name, **options)
|
32
32
|
|
33
33
|
if block
|
34
34
|
# if the transition takes a block we make it available through a proxy on the transition itself!
|
data/lib/railtie.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
namespace :graph do
|
2
|
+
desc 'draw the graph of a state machine'
|
3
|
+
task :draw, [:state_machine_class] => [:environment] do |_, args|
|
4
|
+
state_machine_class = Object.const_get(args.fetch(:state_machine_class))
|
5
|
+
state_machine = state_machine_class.state_machine
|
6
|
+
NxtStateMachine::Graph.new(state_machine).draw
|
7
|
+
end
|
8
|
+
end
|
data/nxt_state_machine.gemspec
CHANGED
@@ -35,12 +35,13 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.require_paths = ["lib"]
|
36
36
|
|
37
37
|
spec.add_dependency "activesupport"
|
38
|
-
spec.add_dependency "nxt_registry", "~> 0.
|
38
|
+
spec.add_dependency "nxt_registry", "~> 0.3.0"
|
39
39
|
|
40
40
|
spec.add_development_dependency "bundler", "~> 2.0"
|
41
|
-
spec.add_development_dependency "rake", "~>
|
41
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
42
42
|
spec.add_development_dependency "rspec", "~> 3.0"
|
43
43
|
spec.add_development_dependency "pry"
|
44
44
|
spec.add_development_dependency "activerecord"
|
45
45
|
spec.add_development_dependency "sqlite3"
|
46
|
+
spec.add_development_dependency "ruby-graphviz"
|
46
47
|
end
|
data/state_machine.png
ADDED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nxt_state_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Robecke
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2020-
|
14
|
+
date: 2020-09-30 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activesupport
|
@@ -33,14 +33,14 @@ dependencies:
|
|
33
33
|
requirements:
|
34
34
|
- - "~>"
|
35
35
|
- !ruby/object:Gem::Version
|
36
|
-
version: 0.
|
36
|
+
version: 0.3.0
|
37
37
|
type: :runtime
|
38
38
|
prerelease: false
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
40
40
|
requirements:
|
41
41
|
- - "~>"
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: 0.
|
43
|
+
version: 0.3.0
|
44
44
|
- !ruby/object:Gem::Dependency
|
45
45
|
name: bundler
|
46
46
|
requirement: !ruby/object:Gem::Requirement
|
@@ -61,14 +61,14 @@ dependencies:
|
|
61
61
|
requirements:
|
62
62
|
- - "~>"
|
63
63
|
- !ruby/object:Gem::Version
|
64
|
-
version: '
|
64
|
+
version: '12.0'
|
65
65
|
type: :development
|
66
66
|
prerelease: false
|
67
67
|
version_requirements: !ruby/object:Gem::Requirement
|
68
68
|
requirements:
|
69
69
|
- - "~>"
|
70
70
|
- !ruby/object:Gem::Version
|
71
|
-
version: '
|
71
|
+
version: '12.0'
|
72
72
|
- !ruby/object:Gem::Dependency
|
73
73
|
name: rspec
|
74
74
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,6 +125,20 @@ dependencies:
|
|
125
125
|
- - ">="
|
126
126
|
- !ruby/object:Gem::Version
|
127
127
|
version: '0'
|
128
|
+
- !ruby/object:Gem::Dependency
|
129
|
+
name: ruby-graphviz
|
130
|
+
requirement: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
128
142
|
description: A state machine library that can be used with ActiveRecord or in plain
|
129
143
|
ruby and should be easy to customize for other integrations
|
130
144
|
email:
|
@@ -139,6 +153,7 @@ files:
|
|
139
153
|
- ".rspec"
|
140
154
|
- ".ruby-version"
|
141
155
|
- ".travis.yml"
|
156
|
+
- CHANGELOG.md
|
142
157
|
- Gemfile
|
143
158
|
- Gemfile.lock
|
144
159
|
- LICENSE.txt
|
@@ -149,6 +164,7 @@ files:
|
|
149
164
|
- lib/nxt_state_machine.rb
|
150
165
|
- lib/nxt_state_machine/callable.rb
|
151
166
|
- lib/nxt_state_machine/callback_registry.rb
|
167
|
+
- lib/nxt_state_machine/defuse_registry.rb
|
152
168
|
- lib/nxt_state_machine/error_callback_registry.rb
|
153
169
|
- lib/nxt_state_machine/errors/error.rb
|
154
170
|
- lib/nxt_state_machine/errors/event_already_registered.rb
|
@@ -165,6 +181,7 @@ files:
|
|
165
181
|
- lib/nxt_state_machine/event.rb
|
166
182
|
- lib/nxt_state_machine/event/names.rb
|
167
183
|
- lib/nxt_state_machine/event_registry.rb
|
184
|
+
- lib/nxt_state_machine/graph.rb
|
168
185
|
- lib/nxt_state_machine/integrations/active_record.rb
|
169
186
|
- lib/nxt_state_machine/integrations/attr_accessor.rb
|
170
187
|
- lib/nxt_state_machine/integrations/hash.rb
|
@@ -176,9 +193,11 @@ files:
|
|
176
193
|
- lib/nxt_state_machine/transition/factory.rb
|
177
194
|
- lib/nxt_state_machine/transition/interface.rb
|
178
195
|
- lib/nxt_state_machine/transition/proxy.rb
|
179
|
-
- lib/nxt_state_machine/transition/store.rb
|
180
196
|
- lib/nxt_state_machine/version.rb
|
197
|
+
- lib/railtie.rb
|
198
|
+
- lib/tasks/draw_graph.rake
|
181
199
|
- nxt_state_machine.gemspec
|
200
|
+
- state_machine.png
|
182
201
|
homepage: https://github.com/nxt-insurance/nxt_state_machine
|
183
202
|
licenses:
|
184
203
|
- MIT
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module NxtStateMachine
|
2
|
-
class Transition::Store < Array
|
3
|
-
def <<(transition)
|
4
|
-
ensure_transition_unique(transition)
|
5
|
-
super
|
6
|
-
end
|
7
|
-
|
8
|
-
alias_method :add, :<<
|
9
|
-
|
10
|
-
private
|
11
|
-
|
12
|
-
def ensure_transition_unique(transition)
|
13
|
-
return unless find { |other| other.from.enum == transition.from.enum && other.to.enum == transition.to.enum }
|
14
|
-
|
15
|
-
raise NxtStateMachine::Errors::TransitionAlreadyRegistered,
|
16
|
-
"A transition from :#{transition.from.enum} to :#{transition.to.enum} was already registered"
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|