nxt_state_machine 0.1.4 → 0.1.9

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: 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