nxt_state_machine 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +56 -0
- data/.editorconfig +15 -0
- data/.ruby-version +1 -1
- data/Gemfile +4 -0
- data/Gemfile.lock +15 -12
- data/README.md +26 -8
- data/lib/nxt_state_machine/callable.rb +5 -3
- data/lib/nxt_state_machine/event.rb +1 -1
- data/lib/nxt_state_machine/integrations/active_record.rb +2 -2
- data/lib/nxt_state_machine/integrations/hash.rb +13 -12
- data/lib/nxt_state_machine/state.rb +24 -3
- data/lib/nxt_state_machine/state_machine.rb +20 -13
- data/lib/nxt_state_machine/transition/around_callback_chain.rb +1 -1
- data/lib/nxt_state_machine/transition/factory.rb +56 -0
- data/lib/nxt_state_machine/transition/interface.rb +15 -0
- data/lib/nxt_state_machine/transition/proxy.rb +1 -1
- data/lib/nxt_state_machine/transition.rb +15 -52
- data/lib/nxt_state_machine/version.rb +1 -1
- data/lib/nxt_state_machine.rb +5 -3
- data/nxt_state_machine.gemspec +1 -1
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 619dd3e81ce389fb16df746e5d940c48d48f6e2c30b2afb108a3a098aa687003
|
4
|
+
data.tar.gz: ee68dba22f251671e019a15f3e36bdbbb04fca5a5649e65fb1d0a43cedb60dbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74cdf937fa1653f1fea76c4cf6a0fb42625b97cf1c3a08dc3ffaef34b9571400c4fc759ee002be24158c58adfeab2b5ef437d2a750b924a10dd8c5af7bccf665
|
7
|
+
data.tar.gz: e1af7824092bb95bfe95b88d71c05b3f8262da0a95364fde1c2a55d31da44dc96113ec31c46539757d96d4a3d112fe12b1e8ba3684bb6740fbdb77a49011004f
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Ruby CircleCI 2.0 configuration file
|
2
|
+
#
|
3
|
+
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
4
|
+
#
|
5
|
+
version: 2
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
docker:
|
9
|
+
# specify the version you desire here
|
10
|
+
- image: circleci/ruby:2.6.5-node
|
11
|
+
environment:
|
12
|
+
BUNDLER_VERSION: 2.0.2
|
13
|
+
|
14
|
+
working_directory: ~/repo
|
15
|
+
|
16
|
+
steps:
|
17
|
+
- checkout
|
18
|
+
|
19
|
+
# Download and cache dependencies
|
20
|
+
- restore_cache:
|
21
|
+
keys:
|
22
|
+
- v1-dependencies-{{ checksum "Gemfile.lock" }}
|
23
|
+
|
24
|
+
- run: gem install bundler --version $BUNDLER_VERSION
|
25
|
+
|
26
|
+
- run:
|
27
|
+
name: install dependencies
|
28
|
+
command: |
|
29
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
30
|
+
|
31
|
+
- save_cache:
|
32
|
+
paths:
|
33
|
+
- ./vendor/bundle
|
34
|
+
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
|
35
|
+
|
36
|
+
# run tests!
|
37
|
+
- run:
|
38
|
+
name: run tests
|
39
|
+
command: |
|
40
|
+
mkdir /tmp/test-results
|
41
|
+
TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
|
42
|
+
circleci tests split --split-by=timings)"
|
43
|
+
|
44
|
+
bundle exec rspec \
|
45
|
+
--format progress \
|
46
|
+
--format RspecJunitFormatter \
|
47
|
+
--out /tmp/test-results/rspec.xml \
|
48
|
+
--format progress \
|
49
|
+
$TEST_FILES
|
50
|
+
|
51
|
+
# collect reports
|
52
|
+
- store_test_results:
|
53
|
+
path: /tmp/test-results
|
54
|
+
- store_artifacts:
|
55
|
+
path: /tmp/test-results
|
56
|
+
destination: test-results
|
data/.editorconfig
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# EditorConfig is awesome: http://EditorConfig.org
|
2
|
+
|
3
|
+
# top-most EditorConfig file
|
4
|
+
root = true
|
5
|
+
|
6
|
+
# Unix-style newlines with a newline ending every file
|
7
|
+
[*]
|
8
|
+
end_of_line = lf
|
9
|
+
insert_final_newline = true
|
10
|
+
charset = utf-8
|
11
|
+
|
12
|
+
[*.{rb,yml,json,rake,erb}]
|
13
|
+
indent_style = space
|
14
|
+
indent_size = 2
|
15
|
+
trim_trailing_whitespace = true
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.6.
|
1
|
+
2.6.5
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
nxt_state_machine (0.1.
|
4
|
+
nxt_state_machine (0.1.1)
|
5
5
|
activesupport
|
6
6
|
nxt_registry (~> 0.1.3)
|
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.2.1)
|
12
|
+
activesupport (= 6.0.2.1)
|
13
|
+
activerecord (6.0.2.1)
|
14
|
+
activemodel (= 6.0.2.1)
|
15
|
+
activesupport (= 6.0.2.1)
|
16
|
+
activesupport (6.0.2.1)
|
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.
|
21
|
+
zeitwerk (~> 2.2)
|
22
22
|
coderay (1.1.2)
|
23
23
|
concurrent-ruby (1.1.5)
|
24
24
|
diff-lcs (1.3)
|
25
25
|
i18n (1.7.0)
|
26
26
|
concurrent-ruby (~> 1.0)
|
27
27
|
method_source (0.9.2)
|
28
|
-
minitest (5.
|
28
|
+
minitest (5.13.0)
|
29
29
|
nxt_registry (0.1.3)
|
30
30
|
activesupport
|
31
31
|
pry (0.12.2)
|
@@ -45,11 +45,13 @@ GEM
|
|
45
45
|
diff-lcs (>= 1.2.0, < 2.0)
|
46
46
|
rspec-support (~> 3.9.0)
|
47
47
|
rspec-support (3.9.0)
|
48
|
-
|
48
|
+
rspec_junit_formatter (0.4.1)
|
49
|
+
rspec-core (>= 2, < 4, != 2.12.0)
|
50
|
+
sqlite3 (1.4.2)
|
49
51
|
thread_safe (0.3.6)
|
50
|
-
tzinfo (1.2.
|
52
|
+
tzinfo (1.2.6)
|
51
53
|
thread_safe (~> 0.1)
|
52
|
-
zeitwerk (2.2.
|
54
|
+
zeitwerk (2.2.2)
|
53
55
|
|
54
56
|
PLATFORMS
|
55
57
|
ruby
|
@@ -61,6 +63,7 @@ DEPENDENCIES
|
|
61
63
|
pry
|
62
64
|
rake (~> 10.0)
|
63
65
|
rspec (~> 3.0)
|
66
|
+
rspec_junit_formatter
|
64
67
|
sqlite3
|
65
68
|
|
66
69
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[![CircleCI](https://circleci.com/gh/nxt-insurance/nxt_state_machine.svg?style=svg)](https://circleci.com/gh/nxt-insurance/nxt_state_machine) [![Depfu](https://badges.depfu.com/badges/e9cb30113bbde657670ab3f5b94cfa67/count.svg)](https://depfu.com/github/nxt-insurance/nxt_state_machine?project_id=10452)
|
2
|
+
|
1
3
|
# NxtStateMachine
|
2
4
|
|
3
5
|
NxtStateMachine is a simple state machine library that ships with an easy to use integration for ActiveRecord.
|
@@ -36,9 +38,9 @@ class ArticleWorkflow
|
|
36
38
|
attr_accessor :article
|
37
39
|
|
38
40
|
state_machine(target: :article, state_attr: :status) do
|
41
|
+
# First we setup the states
|
39
42
|
state :draft, initial: true
|
40
|
-
|
41
|
-
state :submitted
|
43
|
+
states :written, :submitted # define multiple states at the same time
|
42
44
|
state :approved
|
43
45
|
state :published
|
44
46
|
state :rejected
|
@@ -167,6 +169,16 @@ class Article < ApplicationRecord
|
|
167
169
|
end
|
168
170
|
```
|
169
171
|
|
172
|
+
You can also navigate between states
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
state.next # will give you the next state in the order they have been registered
|
176
|
+
state.previous # will give you the previously registered state
|
177
|
+
state.first? # first registered state?
|
178
|
+
state.last? # last registered state?
|
179
|
+
state.index # gives you the index of the state in the registry (can also be overwritten by passing index as an option)
|
180
|
+
```
|
181
|
+
|
170
182
|
### Events
|
171
183
|
|
172
184
|
Once you have defined your states you can define events and their transitions. Events trigger state transitions based
|
@@ -194,7 +206,7 @@ class Article < ApplicationRecord
|
|
194
206
|
# We recommend to use keyword arguments to make events accept custom arguments
|
195
207
|
transition from: %i[written rejected], to: :approved do |approved_at:|
|
196
208
|
self.approved_at = approved_at
|
197
|
-
# NOTE: The transition is halted if this returns a falsey value
|
209
|
+
# NOTE: The transition is NOT halted if this returns a falsey value
|
198
210
|
end
|
199
211
|
end
|
200
212
|
end
|
@@ -216,8 +228,12 @@ article.approve(approved_at: Time.current)
|
|
216
228
|
article.approve!(approved_at: Time.current)
|
217
229
|
```
|
218
230
|
|
219
|
-
**NOTE:** Transitions run in transactions that
|
220
|
-
|
231
|
+
**NOTE:** Transitions run in transactions that acquire a lock to prevent concurrency issues. Transactions will be
|
232
|
+
rolled back in case of an exception or if your target cannot be saved due to validation errors.
|
233
|
+
The state is set back to the state before the transition! If you try to transitions on records with unpersisted changes
|
234
|
+
you will get a `RuntimeError: Locking a record with unpersisted changes is not supported.` error saying something
|
235
|
+
like `Use :save to persist the changes, or :reload to discard them explicitly.` since it's not possible to acquire a
|
236
|
+
lock on modified records.
|
221
237
|
|
222
238
|
### Transitions
|
223
239
|
|
@@ -255,7 +271,7 @@ Transitions can be halted in callbacks and during the transition itself simply b
|
|
255
271
|
You can register `before_transition`, `around_transition` and `after_transition` callbacks. By defining the
|
256
272
|
:from and :to states you decide on which transitions the callback actually runs. Around callbacks need to call the
|
257
273
|
proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top level
|
258
|
-
|
274
|
+
behaves exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is
|
259
275
|
the :from and :to parameters with which they are registered.
|
260
276
|
|
261
277
|
|
@@ -327,10 +343,12 @@ end
|
|
327
343
|
|
328
344
|
## TODO
|
329
345
|
- Test implementations for Hash, AttrAccessor
|
346
|
+
- Thread safety spec!
|
347
|
+
- Spec locks?
|
348
|
+
- Explain locking in readme!
|
349
|
+
- Should we clone machines for each context?
|
330
350
|
- What about inheritance? => What would be the expected behaviour? (dup vs. no dup)
|
331
351
|
=> 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
352
|
|
335
353
|
|
336
354
|
## Development
|
@@ -13,16 +13,18 @@ module NxtStateMachine
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def bind(execution_context = nil)
|
17
17
|
self.context = execution_context
|
18
18
|
ensure_context_not_missing
|
19
19
|
self
|
20
20
|
end
|
21
21
|
|
22
|
-
|
22
|
+
# NOTE: allowing call(*args, **opts) is dangerous when called with a hash as an argument!
|
23
|
+
# It will automatically become the **opts which might not be what you want! Probably better
|
24
|
+
# to introduce arguments: [], options: { } or something
|
25
|
+
def call(*args)
|
23
26
|
ensure_context_not_missing
|
24
27
|
|
25
|
-
args << opts
|
26
28
|
args = args.take(arity)
|
27
29
|
|
28
30
|
if method?
|
@@ -26,7 +26,7 @@ module NxtStateMachine
|
|
26
26
|
|
27
27
|
def transitions(from:, to:, &block)
|
28
28
|
Array(from).each do |from_state|
|
29
|
-
transition = Transition.new(name, from: from_state, to: to, state_machine: state_machine, &block)
|
29
|
+
transition = Transition::Factory.new(name, from: from_state, to: to, state_machine: state_machine, &block)
|
30
30
|
state_machine.transitions << transition
|
31
31
|
event_transitions.register(from_state, transition)
|
32
32
|
end
|
@@ -21,7 +21,7 @@ module NxtStateMachine
|
|
21
21
|
end
|
22
22
|
|
23
23
|
machine.set_state_with do |target, transition|
|
24
|
-
target.
|
24
|
+
target.with_lock do
|
25
25
|
transition.run_before_callbacks
|
26
26
|
result = set_state(target, transition, state_attr, :save)
|
27
27
|
transition.run_after_callbacks
|
@@ -39,7 +39,7 @@ module NxtStateMachine
|
|
39
39
|
end
|
40
40
|
|
41
41
|
machine.set_state_with! do |target, transition|
|
42
|
-
target.
|
42
|
+
target.with_lock do
|
43
43
|
transition.run_before_callbacks
|
44
44
|
result = set_state(target, transition, state_attr, :save!)
|
45
45
|
transition.run_after_callbacks
|
@@ -9,21 +9,21 @@ module NxtStateMachine
|
|
9
9
|
&config
|
10
10
|
)
|
11
11
|
|
12
|
-
machine.get_state_with do |
|
13
|
-
if
|
14
|
-
|
12
|
+
machine.get_state_with do |current_target|
|
13
|
+
if current_target[state_attr].nil?
|
14
|
+
current_target[state_attr] = initial_state.enum
|
15
15
|
end
|
16
16
|
|
17
|
-
|
17
|
+
current_target[state_attr]
|
18
18
|
end
|
19
19
|
|
20
|
-
machine.set_state_with do |
|
20
|
+
machine.set_state_with do |current_target, transition|
|
21
21
|
transition.run_before_callbacks
|
22
|
-
result = set_state(
|
22
|
+
result = set_state(current_target, transition, state_attr)
|
23
23
|
transition.run_after_callbacks
|
24
24
|
result
|
25
25
|
rescue StandardError => error
|
26
|
-
|
26
|
+
current_target[state_attr] = transition.from.enum
|
27
27
|
|
28
28
|
if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
|
29
29
|
false
|
@@ -32,14 +32,14 @@ module NxtStateMachine
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
machine.set_state_with! do |
|
35
|
+
machine.set_state_with! do |current_target, transition|
|
36
36
|
transition.run_before_callbacks
|
37
|
-
result = set_state(
|
37
|
+
result = set_state(current_target, transition, state_attr)
|
38
38
|
transition.run_after_callbacks
|
39
39
|
|
40
40
|
result
|
41
41
|
rescue StandardError
|
42
|
-
|
42
|
+
current_target[state_attr] = transition.from.enum
|
43
43
|
raise
|
44
44
|
end
|
45
45
|
|
@@ -50,10 +50,10 @@ module NxtStateMachine
|
|
50
50
|
module InstanceMethods
|
51
51
|
private
|
52
52
|
|
53
|
-
def set_state(
|
53
|
+
def set_state(current_target, transition, state_attr)
|
54
54
|
transition.execute do |block|
|
55
55
|
result = block ? block.call : nil
|
56
|
-
set_state_result =
|
56
|
+
set_state_result = current_target[state_attr] = transition.to.enum || halt_transition
|
57
57
|
block ? result : set_state_result
|
58
58
|
end
|
59
59
|
end
|
@@ -61,6 +61,7 @@ module NxtStateMachine
|
|
61
61
|
|
62
62
|
def self.included(base)
|
63
63
|
base.include(NxtStateMachine)
|
64
|
+
base.include(InstanceMethods)
|
64
65
|
base.extend(ClassMethods)
|
65
66
|
end
|
66
67
|
end
|
@@ -1,17 +1,38 @@
|
|
1
1
|
module NxtStateMachine
|
2
2
|
class State
|
3
|
-
def initialize(enum,
|
3
|
+
def initialize(enum, state_machine, **opts)
|
4
4
|
@enum = enum
|
5
|
-
@
|
5
|
+
@state_machine = state_machine
|
6
6
|
@initial = opts.delete(:initial)
|
7
7
|
@transitions = []
|
8
8
|
@options = opts.with_indifferent_access
|
9
|
+
@index = opts.fetch(:index)
|
9
10
|
end
|
10
11
|
|
11
|
-
attr_accessor :enum, :initial, :transitions, :
|
12
|
+
attr_accessor :enum, :initial, :index, :transitions, :state_machine, :options
|
12
13
|
|
13
14
|
def to_s
|
14
15
|
enum.to_s
|
15
16
|
end
|
17
|
+
|
18
|
+
def previous
|
19
|
+
previous_index = (index - 1) % state_machine.states.size
|
20
|
+
key = state_machine.states.keys[previous_index]
|
21
|
+
state_machine.states.resolve(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def next
|
25
|
+
next_index = (index + 1) % state_machine.states.size
|
26
|
+
key = state_machine.states.keys[next_index]
|
27
|
+
state_machine.states.resolve(key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def last?
|
31
|
+
index == state_machine.states.size - 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def first?
|
35
|
+
index == 0
|
36
|
+
end
|
16
37
|
end
|
17
38
|
end
|
@@ -14,24 +14,24 @@ module NxtStateMachine
|
|
14
14
|
@initial_state = nil
|
15
15
|
end
|
16
16
|
|
17
|
-
attr_reader :class_context, :
|
17
|
+
attr_reader :class_context, :transitions, :events, :options, :callbacks, :name, :error_callback_registry
|
18
18
|
attr_accessor :initial_state
|
19
19
|
|
20
20
|
def get_state_with(method = nil, &block)
|
21
21
|
method_or_block = (method || block)
|
22
|
-
@get_state_with ||= method_or_block
|
22
|
+
@get_state_with ||= method_or_block ||
|
23
23
|
raise_missing_configuration_error(:get_state_with)
|
24
24
|
end
|
25
25
|
|
26
26
|
def set_state_with(method = nil, &block)
|
27
27
|
method_or_block = (method || block)
|
28
|
-
@set_state_with ||= method_or_block
|
28
|
+
@set_state_with ||= method_or_block ||
|
29
29
|
raise_missing_configuration_error(:set_state_with)
|
30
30
|
end
|
31
31
|
|
32
32
|
def set_state_with!(method = nil, &block)
|
33
33
|
method_or_block = (method || block)
|
34
|
-
@set_state_with_bang ||= method_or_block
|
34
|
+
@set_state_with_bang ||= method_or_block ||
|
35
35
|
raise_missing_configuration_error(:set_state_with!)
|
36
36
|
end
|
37
37
|
|
@@ -44,12 +44,11 @@ module NxtStateMachine
|
|
44
44
|
if opts.fetch(:initial) && initial_state.present?
|
45
45
|
raise NxtStateMachine::Errors::InitialStateAlreadyDefined, ":#{initial_state.enum} was already defined as the initial state"
|
46
46
|
else
|
47
|
-
state = new_state_class(&block).new(name, self, opts)
|
47
|
+
state = new_state_class(&block).new(name, self, **opts.reverse_merge(index: states.size))
|
48
48
|
states.register(name, state)
|
49
49
|
self.initial_state = state if opts.fetch(:initial)
|
50
50
|
|
51
51
|
class_context.define_method "#{name}?" do
|
52
|
-
# States internally are always strings
|
53
52
|
machine.current_state_name(self) == name
|
54
53
|
end
|
55
54
|
|
@@ -58,6 +57,13 @@ module NxtStateMachine
|
|
58
57
|
end
|
59
58
|
end
|
60
59
|
|
60
|
+
def states(*names, **opts, &block)
|
61
|
+
# method overloading in ruby ;-)
|
62
|
+
return @states unless names.any?
|
63
|
+
|
64
|
+
state(*names, **opts, &block)
|
65
|
+
end
|
66
|
+
|
61
67
|
def transitions
|
62
68
|
@transitions ||= events.values.flat_map(&:event_transitions)
|
63
69
|
end
|
@@ -77,19 +83,20 @@ module NxtStateMachine
|
|
77
83
|
end
|
78
84
|
|
79
85
|
def event(name, &block)
|
86
|
+
name = name.to_sym
|
80
87
|
event = Event.new(name, state_machine: self, &block)
|
81
88
|
events.register(name, event)
|
82
89
|
|
83
90
|
class_context.define_method name do |*args, **opts|
|
84
91
|
event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
|
85
92
|
transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
|
86
|
-
transition.
|
93
|
+
transition.build_transition(name, self, :set_state_with, *args, **opts)
|
87
94
|
end
|
88
95
|
|
89
96
|
class_context.define_method "#{name}!" do |*args, **opts|
|
90
97
|
event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
|
91
98
|
transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
|
92
|
-
transition.
|
99
|
+
transition.build_transition("#{name}!".to_sym, self, :set_state_with!, *args, **opts)
|
93
100
|
end
|
94
101
|
|
95
102
|
class_context.define_method "can_#{name}?" do
|
@@ -98,8 +105,7 @@ module NxtStateMachine
|
|
98
105
|
end
|
99
106
|
|
100
107
|
def can_transition?(event_name, from)
|
101
|
-
|
102
|
-
event = events.resolve(normalized_event_name)
|
108
|
+
event = events.resolve(event_name)
|
103
109
|
event && event.event_transitions.key?(from)
|
104
110
|
end
|
105
111
|
|
@@ -133,6 +139,7 @@ module NxtStateMachine
|
|
133
139
|
self
|
134
140
|
end
|
135
141
|
|
142
|
+
# TODO: Everything that require context should live in some sort of proxy
|
136
143
|
def run_before_callbacks(transition, context)
|
137
144
|
run_callbacks(transition, :before, context)
|
138
145
|
end
|
@@ -149,16 +156,16 @@ module NxtStateMachine
|
|
149
156
|
current_callbacks = callbacks.resolve(transition, kind)
|
150
157
|
|
151
158
|
current_callbacks.each do |callback|
|
152
|
-
Callable.new(callback).
|
159
|
+
Callable.new(callback).bind(context).call(transition)
|
153
160
|
end
|
154
161
|
end
|
155
162
|
|
156
163
|
def current_state_name(context)
|
157
|
-
get_state_with.
|
164
|
+
Callable.new(get_state_with).bind(context).call(target(context))
|
158
165
|
end
|
159
166
|
|
160
167
|
def target(context)
|
161
|
-
@target_method ||= options[:target] || :itself
|
168
|
+
@target_method ||= (options[:target] || :itself)
|
162
169
|
context.send(@target_method)
|
163
170
|
end
|
164
171
|
|
@@ -10,7 +10,7 @@ module NxtStateMachine
|
|
10
10
|
def build(proxy)
|
11
11
|
return proxy unless callbacks.any?
|
12
12
|
|
13
|
-
callbacks.map { |c| Callable.new(c).
|
13
|
+
callbacks.map { |c| Callable.new(c).bind(context) }.reverse.inject(proxy) do |previous, callback|
|
14
14
|
-> { callback.call(previous, transition) }
|
15
15
|
end
|
16
16
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition::Factory
|
3
|
+
include Transition::Interface
|
4
|
+
|
5
|
+
def initialize(name, from:, to:, state_machine:, &block)
|
6
|
+
@name = name
|
7
|
+
@from = state_machine.states.resolve(from)
|
8
|
+
@to = state_machine.states.resolve(to)
|
9
|
+
@state_machine = state_machine
|
10
|
+
@block = block
|
11
|
+
|
12
|
+
# TODO: Write a spec that verifies that transitions are unique
|
13
|
+
ensure_states_exist
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :name, :from, :to
|
17
|
+
|
18
|
+
# TODO: Probably would make sense if we could also define the event name to be passed in
|
19
|
+
# => This way we could differentiate what event triggered the callback!!!
|
20
|
+
|
21
|
+
def build_transition(event, context, set_state_method, *args, **opts)
|
22
|
+
options = {
|
23
|
+
from: from,
|
24
|
+
to: to,
|
25
|
+
state_machine: state_machine,
|
26
|
+
context: context,
|
27
|
+
event: event,
|
28
|
+
set_state_method: set_state_method
|
29
|
+
}
|
30
|
+
|
31
|
+
transition = Transition.new(name, **options)
|
32
|
+
|
33
|
+
if block
|
34
|
+
# if the transition takes a block we make it available through a proxy on the transition itself!
|
35
|
+
transition.send(:block=, Proc.new do
|
36
|
+
# if the block takes arguments we always pass the transition as the first one
|
37
|
+
args.prepend(transition) if block.arity > 0
|
38
|
+
context.instance_exec(*args, **opts, &block)
|
39
|
+
end)
|
40
|
+
end
|
41
|
+
|
42
|
+
transition.prepare
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
delegate :all_states, :any_states, to: :state_machine
|
48
|
+
|
49
|
+
attr_reader :block, :state_machine
|
50
|
+
|
51
|
+
def ensure_states_exist
|
52
|
+
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{from} registered" unless state_machine.states.key?(from.enum)
|
53
|
+
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{to} registered" unless state_machine.states.key?(to.enum)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module NxtStateMachine
|
2
|
+
class Transition
|
3
|
+
module Interface
|
4
|
+
def id
|
5
|
+
@id ||= "#{from.to_s}_#{to.to_s}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def transitions_from_to?(from_state, to_state)
|
9
|
+
from.enum.in?(Array(from_state)) && to.enum.in?(Array(to_state))
|
10
|
+
end
|
11
|
+
|
12
|
+
delegate :all_states, :any_states, to: :state_machine
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,54 +1,35 @@
|
|
1
1
|
module NxtStateMachine
|
2
2
|
class Transition
|
3
|
-
|
3
|
+
include Transition::Interface
|
4
|
+
|
5
|
+
def initialize(name, event:, from:, to:, state_machine:, context:, set_state_method:, &block)
|
4
6
|
@name = name
|
7
|
+
@event = event
|
5
8
|
@from = state_machine.states.resolve(from)
|
6
9
|
@to = state_machine.states.resolve(to)
|
7
10
|
@state_machine = state_machine
|
11
|
+
@set_state_method = set_state_method
|
12
|
+
@context = context
|
8
13
|
@block = block
|
9
|
-
|
10
|
-
# TODO: Write a spec that verifies that transitions are unique
|
11
|
-
ensure_states_exist
|
12
14
|
end
|
13
15
|
|
14
|
-
attr_reader :name, :from, :to
|
15
|
-
|
16
|
-
# TODO: Probably would make sense if we could also define the event name to be passed in
|
17
|
-
# => This way we could differentiate what event triggered the callback!!!
|
18
|
-
|
19
|
-
def prepare(event, context, set_state_with_method, *args, **opts)
|
20
|
-
# This exposes the transition block on the transition_to_execute so it can be executed later in :set_state_with
|
21
|
-
current_transition = clone
|
22
|
-
current_transition.send(:context=, context)
|
23
|
-
current_transition.send(:event=, event)
|
24
|
-
current_transition.send(:block_proxy=, nil)
|
16
|
+
attr_reader :name, :from, :to, :block, :event
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
args.prepend(current_transition) if block.arity > 0
|
31
|
-
context.instance_exec(*args, **opts, &block)
|
32
|
-
end
|
33
|
-
|
34
|
-
current_transition.send(:block_proxy=, proxy)
|
35
|
-
end
|
36
|
-
|
37
|
-
state_machine.send(
|
38
|
-
set_state_with_method
|
39
|
-
).with_context(
|
18
|
+
def prepare
|
19
|
+
Callable.new(
|
20
|
+
state_machine.send(set_state_method)
|
21
|
+
).bind(
|
40
22
|
context
|
41
|
-
).call(state_machine.target(context),
|
23
|
+
).call(state_machine.target(context), self)
|
42
24
|
end
|
43
25
|
|
44
26
|
def execute(&block)
|
45
|
-
# This is called on the cloned transition from above!
|
46
27
|
Transition::Proxy.new(event, state_machine,self, context).call(&block)
|
47
28
|
rescue StandardError => error
|
48
29
|
callback = state_machine.find_error_callback(error, self)
|
49
30
|
raise unless callback
|
50
31
|
|
51
|
-
Callable.new(callback).
|
32
|
+
Callable.new(callback).bind(context).call(error, self)
|
52
33
|
end
|
53
34
|
|
54
35
|
alias_method :with_around_callbacks, :execute
|
@@ -61,27 +42,9 @@ module NxtStateMachine
|
|
61
42
|
state_machine.run_after_callbacks(self, context)
|
62
43
|
end
|
63
44
|
|
64
|
-
def transitions_from_to?(from_state, to_state)
|
65
|
-
from.enum.in?(Array(from_state)) && to.enum.in?(Array(to_state))
|
66
|
-
end
|
67
|
-
|
68
|
-
def id
|
69
|
-
@id ||= "#{from.to_s}_#{to.to_s}"
|
70
|
-
end
|
71
|
-
|
72
|
-
attr_reader :block_proxy, :event
|
73
|
-
|
74
45
|
private
|
75
46
|
|
76
|
-
|
77
|
-
|
78
|
-
attr_reader :block, :state_machine
|
79
|
-
attr_accessor :context
|
80
|
-
attr_writer :block_proxy, :event
|
81
|
-
|
82
|
-
def ensure_states_exist
|
83
|
-
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{from} registered" unless state_machine.states.key?(from.enum)
|
84
|
-
raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{to} registered" unless state_machine.states.key?(to.enum)
|
85
|
-
end
|
47
|
+
attr_reader :state_machine, :set_state_method, :context
|
48
|
+
attr_writer :block
|
86
49
|
end
|
87
50
|
end
|
data/lib/nxt_state_machine.rb
CHANGED
@@ -19,7 +19,9 @@ require "nxt_state_machine/error_callback_registry"
|
|
19
19
|
require "nxt_state_machine/event_registry"
|
20
20
|
require "nxt_state_machine/state"
|
21
21
|
require "nxt_state_machine/event"
|
22
|
+
require "nxt_state_machine/transition/interface"
|
22
23
|
require "nxt_state_machine/transition"
|
24
|
+
require "nxt_state_machine/transition/factory"
|
23
25
|
require "nxt_state_machine/transition/proxy"
|
24
26
|
require "nxt_state_machine/transition/store"
|
25
27
|
require "nxt_state_machine/transition/around_callback_chain"
|
@@ -71,15 +73,15 @@ module NxtStateMachine
|
|
71
73
|
end
|
72
74
|
|
73
75
|
def state_machine(name = :default)
|
74
|
-
@state_machine ||= self.class.state_machines
|
76
|
+
@state_machine ||= self.class.state_machines.resolve(name)
|
75
77
|
end
|
76
78
|
|
77
79
|
def current_state_name(name = :default)
|
78
|
-
state_machines
|
80
|
+
state_machines.resolve(name).current_state_name(self)
|
79
81
|
end
|
80
82
|
|
81
83
|
def current_state(name = :default)
|
82
|
-
state_machines
|
84
|
+
state_machines.resolve(name).states.resolve(current_state_name(name))
|
83
85
|
end
|
84
86
|
|
85
87
|
def halt_transition(*args, **opts)
|
data/nxt_state_machine.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
|
11
11
|
spec.summary = %q{A rich but straight forward state machine library}
|
12
12
|
spec.description = %q{A state machine library that can be used with ActiveRecord or in plain ruby and should be easy to customize for other integrations}
|
13
|
-
spec.homepage = "https://github.com/nxt-insurance"
|
13
|
+
spec.homepage = "https://github.com/nxt-insurance/nxt_state_machine"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
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.1
|
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-01-
|
14
|
+
date: 2020-01-07 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activesupport
|
@@ -133,6 +133,8 @@ executables: []
|
|
133
133
|
extensions: []
|
134
134
|
extra_rdoc_files: []
|
135
135
|
files:
|
136
|
+
- ".circleci/config.yml"
|
137
|
+
- ".editorconfig"
|
136
138
|
- ".gitignore"
|
137
139
|
- ".rspec"
|
138
140
|
- ".ruby-version"
|
@@ -169,16 +171,18 @@ files:
|
|
169
171
|
- lib/nxt_state_machine/state_registry.rb
|
170
172
|
- lib/nxt_state_machine/transition.rb
|
171
173
|
- lib/nxt_state_machine/transition/around_callback_chain.rb
|
174
|
+
- lib/nxt_state_machine/transition/factory.rb
|
175
|
+
- lib/nxt_state_machine/transition/interface.rb
|
172
176
|
- lib/nxt_state_machine/transition/proxy.rb
|
173
177
|
- lib/nxt_state_machine/transition/store.rb
|
174
178
|
- lib/nxt_state_machine/version.rb
|
175
179
|
- nxt_state_machine.gemspec
|
176
|
-
homepage: https://github.com/nxt-insurance
|
180
|
+
homepage: https://github.com/nxt-insurance/nxt_state_machine
|
177
181
|
licenses:
|
178
182
|
- MIT
|
179
183
|
metadata:
|
180
184
|
allowed_push_host: https://rubygems.org
|
181
|
-
homepage_uri: https://github.com/nxt-insurance
|
185
|
+
homepage_uri: https://github.com/nxt-insurance/nxt_state_machine
|
182
186
|
source_code_uri: https://github.com/nxt-insurance/nxt_state_machine
|
183
187
|
post_install_message:
|
184
188
|
rdoc_options: []
|
@@ -195,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
195
199
|
- !ruby/object:Gem::Version
|
196
200
|
version: '0'
|
197
201
|
requirements: []
|
198
|
-
rubygems_version: 3.0.
|
202
|
+
rubygems_version: 3.0.3
|
199
203
|
signing_key:
|
200
204
|
specification_version: 4
|
201
205
|
summary: A rich but straight forward state machine library
|