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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5b521a6ccdd3c6538bb1941f4cbccce8a7e83759a2f5d0c6417d2e479ee6a94
4
- data.tar.gz: a79d3f8f70987524e2246650e646cef2978baf869ccd42b198e389033c0ff645
3
+ metadata.gz: 619dd3e81ce389fb16df746e5d940c48d48f6e2c30b2afb108a3a098aa687003
4
+ data.tar.gz: ee68dba22f251671e019a15f3e36bdbbb04fca5a5649e65fb1d0a43cedb60dbf
5
5
  SHA512:
6
- metadata.gz: 2bc0e9e567a1cef8ba5c98e05d635491ec282f7b548a27172d9972d4ae57dbed753f93f9502560cf430405a7789cf29b12e8425a55f1dfc09e4cd79031e8957f
7
- data.tar.gz: 6c0f560702961e7afa1025fb33a57ff57df0ab05ba543abef568eb7ca987dcd738586a25e06ae5b538f7b01f9b6043322c4bee19a8df9c9471f323206378e5c0
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
1
+ 2.6.5
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in nxt_state_machine.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'rspec_junit_formatter' # for CircleCI
8
+ end
data/Gemfile.lock CHANGED
@@ -1,31 +1,31 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nxt_state_machine (0.1.0)
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.0)
12
- activesupport (= 6.0.0)
13
- activerecord (6.0.0)
14
- activemodel (= 6.0.0)
15
- activesupport (= 6.0.0)
16
- activesupport (6.0.0)
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.1, >= 2.1.8)
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.12.2)
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
- sqlite3 (1.4.1)
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.5)
52
+ tzinfo (1.2.6)
51
53
  thread_safe (~> 0.1)
52
- zeitwerk (2.2.0)
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
- state :written
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 will be rolled back in case of an exception or if your target cannot be
220
- saved due to validation errors. The state is then set back to the state before the transition!
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
- behavious exactly the same way and is only a matter of structure. The only thing that defines when callbacks run is
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 with_context(execution_context = nil)
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
- def call(*args, **opts)
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.transaction do
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.transaction do
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 |target|
13
- if target[state_attr].nil?
14
- target[state_attr] = initial_state.enum
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
- target[state_attr]
17
+ current_target[state_attr]
18
18
  end
19
19
 
20
- machine.set_state_with do |target, transition|
20
+ machine.set_state_with do |current_target, transition|
21
21
  transition.run_before_callbacks
22
- result = set_state(target, transition, state_attr)
22
+ result = set_state(current_target, transition, state_attr)
23
23
  transition.run_after_callbacks
24
24
  result
25
25
  rescue StandardError => error
26
- target[state_attr] = transition.from.enum
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 |target, transition|
35
+ machine.set_state_with! do |current_target, transition|
36
36
  transition.run_before_callbacks
37
- result = set_state(target, transition, state_attr)
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
- target[state_attr] = transition.from.enum
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(target, transition, state_attr)
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 = target[state_attr] = transition.to.enum || halt_transition
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, machine, **opts)
3
+ def initialize(enum, state_machine, **opts)
4
4
  @enum = enum
5
- @machine = machine
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, :machine, :options
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, :states, :transitions, :events, :options, :callbacks, :name, :error_callback_registry
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 && Callable.new(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 && Callable.new(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 && Callable.new(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.prepare(name, self, :set_state_with, *args, **opts)
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.prepare("#{name}!", self, :set_state_with!, *args, **opts)
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
- normalized_event_name = event_name
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).with_context(context).call(transition)
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.with_context(context).call(target(context))
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).with_context(context) }.reverse.inject(proxy) do |previous, callback|
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
@@ -10,7 +10,7 @@ module NxtStateMachine
10
10
  def call(&block)
11
11
  proxy = if block.arity == 1
12
12
  Proc.new do
13
- block.call(transition.block_proxy)
13
+ block.call(transition.block)
14
14
  end
15
15
  else
16
16
  block
@@ -1,54 +1,35 @@
1
1
  module NxtStateMachine
2
2
  class Transition
3
- def initialize(name, from:, to:, state_machine:, &block)
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
- # block_proxy only is set when the transition accepts a block!
27
- if block
28
- proxy = Proc.new do
29
- # if the block takes arguments we always pass the transition as the first one
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), current_transition)
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).with_context(context).call(error, self)
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
- delegate :all_states, to: :state_machine
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
@@ -1,3 +1,3 @@
1
1
  module NxtStateMachine
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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[name]
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[name].current_state_name(self)
80
+ state_machines.resolve(name).current_state_name(self)
79
81
  end
80
82
 
81
83
  def current_state(name = :default)
82
- state_machines[name].states.fetch(current_state_name(name))
84
+ state_machines.resolve(name).states.resolve(current_state_name(name))
83
85
  end
84
86
 
85
87
  def halt_transition(*args, **opts)
@@ -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.0
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-03 00:00:00.000000000 Z
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.6
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