nxt_state_machine 0.1.0 → 0.1.1
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 +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
|
+
[](https://circleci.com/gh/nxt-insurance/nxt_state_machine) [](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
|