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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c035f94c9cd82caa184fde21b45cba13ad4791b655a34f8cb5140898f7e0760a
4
- data.tar.gz: a7237abef06bf937730f6f848a57e276c82dbf6d605f6a34889303402c498655
3
+ metadata.gz: 6128e445380f2ed2b836b4c23ed58556efd2ff0728add08c2cbd45e97812e068
4
+ data.tar.gz: 05c8861e99afefec3c654366050b225d7a93c10ff984e508cd14d6345b1d0ff3
5
5
  SHA512:
6
- metadata.gz: 67822ebb580c7398d81e6be7c228fc5de573e3ec757843a8f501ea10e7926ae14b5194ce994e79927a5557bc4bbb200f1d05e9081c470b7ac9a9d202a4cc2f21
7
- data.tar.gz: 384e7c4e66c21a3b7fe6a043bc1b8936f6061605a4de0d7788f003b9982f96ecb709954c3988165936a3dad0bcfb82c5c781bc0815070ca9ad43a8d46ba1d354
6
+ metadata.gz: d3038d32da3a7bfa5a49b4537e7c6f163259ddd0f3d2b48a05bfa838021350f648d75bfc493125529c9e1374dc34aa2c9abe94325790fe4ba82a09f01e2afdc8
7
+ data.tar.gz: 64178102f125d3b180f31b1a0bbebd766226b44351bbef906d907cc6339f2ff213c743c51b52238706827cfd597e410b828eb0824f8a4747f363c9b2b79a0ce9
@@ -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
@@ -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)
@@ -1,57 +1,60 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nxt_state_machine (0.1.4)
4
+ nxt_state_machine (0.1.9)
5
5
  activesupport
6
- nxt_registry (~> 0.1.3)
6
+ nxt_registry (~> 0.3.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
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)
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.2)
23
- concurrent-ruby (1.1.5)
24
- diff-lcs (1.3)
25
- i18n (1.8.2)
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.9.2)
28
- minitest (5.14.0)
29
- nxt_registry (0.1.4)
27
+ method_source (1.0.0)
28
+ minitest (5.14.2)
29
+ nxt_registry (0.3.2)
30
30
  activesupport
31
- pry (0.12.2)
32
- coderay (~> 1.1.0)
33
- method_source (~> 0.9.0)
34
- rake (10.5.0)
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.0)
40
- rspec-support (~> 3.9.0)
41
- rspec-expectations (3.9.0)
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.0)
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.0)
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.6)
55
+ tzinfo (1.2.7)
53
56
  thread_safe (~> 0.1)
54
- zeitwerk (2.2.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 (~> 10.0)
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. Transactions will be
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 transitions on records with unpersisted changes
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 `after_transition` callbacks. By defining the
281
- :from and :to states you decide on which transitions the callback actually runs. Around callbacks need to call the
282
- proc object that they get passed in. Registering callbacks inside an event block or on the state_machine top level
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
- ### Multiple state machines in the same class
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
@@ -3,4 +3,4 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
@@ -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!(name) || state_machines.register(
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
- nested :to do
28
- nested :kind, default: -> { [] } do
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
@@ -29,8 +29,8 @@ module NxtStateMachine
29
29
 
30
30
  def callbacks
31
31
  @callbacks ||= registry :from do
32
- nested :to do
33
- nested :error, transform_keys: false, call: false
32
+ level :to do
33
+ level :error, transform_keys: false, call: false
34
34
  end
35
35
  end
36
36
  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!(state).present?
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.with_lock do
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.with_lock do
43
- transition.run_before_callbacks
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
- result
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, method)
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(method) || halt_transition
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 = Transition::Store.new
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, :transitions, :events, :options, :callbacks, :name, :error_callback_registry
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
- transition.build_transition(event_name, self, set_state_method, *args, **opts)
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!
@@ -1,3 +1,3 @@
1
1
  module NxtStateMachine
2
- VERSION = "0.1.4"
2
+ VERSION = '0.1.9'
3
3
  end
@@ -0,0 +1,8 @@
1
+ class Railtie < Rails::Railtie
2
+ railtie_name :nxt_state_machine
3
+
4
+ rake_tasks do
5
+ path = File.expand_path(__dir__)
6
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
7
+ end
8
+ end
@@ -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
@@ -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.1.3"
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", "~> 10.0"
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
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
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-02-07 00:00:00.000000000 Z
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.1.3
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.1.3
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: '10.0'
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: '10.0'
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