nxt_state_machine 0.1.6 → 0.1.10

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: 77b32ce5c0f8c968180650e6497e84076803948c832e84d5c63145ff10050c46
4
- data.tar.gz: 5a5f210aaf9b8f0d8278a66851f4d3be8f278a4a448975252610b3eebe292b06
3
+ metadata.gz: c131c9fdbce2f5a605ecdfa39a63e0ea7ef21dda4a2f9600e790e56e0c4444a0
4
+ data.tar.gz: 49689f892d2262705c0248c05d7b0d6612b31255fb3da130494cbe200f105d83
5
5
  SHA512:
6
- metadata.gz: fbbf6e38dc538a6e5018d9839c591ddf707cc5d348c310582680e8cf588715545214ebe1b16ab70dddad63a0cfa97ab7d66121752ffc1034813128fdb0f2e1e7
7
- data.tar.gz: ef95346d040eb48905820314241e67009d846c00c71725cc2ab24948db0e426d7cdddb1fd55c01d7e63a15d465d0342adf9d35f888d6c62d1d21ba596f19bd54
6
+ metadata.gz: e176a5b1205cbfb3d901f5c7c768971b64eea974f613d4a298430f81473c13dc3852f6e4eb021d4fd1eb98cba2fd8b9d69a4671a64b0c9a5e1f2e46cc20c7580
7
+ data.tar.gz: 9dff03854918e23c0c0dc9ae00c55116a4244a314f0799ce2e3c81841d257e9406cff25eb68833c6c32a99e8d4c562ff1609d22fb307c63f44a12165f47a2997
data/.circleci/config.yml CHANGED
@@ -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
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.5
1
+ 2.7.3
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # v0.1.10 2021-07-19
2
+
3
+ ### Added
4
+
5
+ - Transition now optionally get passed the transition object that allows to access transition arguments and options.
6
+
7
+ [Compare v0.1.9...v0.1.10](https://github.com/nxt-insurance/nxt_state_machine/compare/v0.1.9...v0.1.10)
8
+
9
+
10
+ # v0.1.9 2020-09-23
11
+
12
+ ### Added
13
+
14
+ - Allow to toggle locking of transitions for active record adapter per event or globally
15
+
16
+ [Compare v0.1.8...v0.1.9](https://github.com/nxt-insurance/nxt_state_machine/compare/v0.1.8...v0.1.9)
17
+
18
+
19
+ # v0.1.8 2020-0-05
20
+
21
+ ### Added
22
+
23
+ - [internal] Allow to draw state machine graph with rake task
24
+
25
+ [Compare v0.1.7...v0.1.8](https://github.com/nxt-insurance/nxt_state_machine/compare/v0.1.7...v0.1.8)
data/Gemfile.lock CHANGED
@@ -1,57 +1,59 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nxt_state_machine (0.1.6)
4
+ nxt_state_machine (0.1.10)
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.1.3.2)
12
+ activesupport (= 6.1.3.2)
13
+ activerecord (6.1.3.2)
14
+ activemodel (= 6.1.3.2)
15
+ activesupport (= 6.1.3.2)
16
+ activesupport (6.1.3.2)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 0.7, < 2)
19
- minitest (~> 5.1)
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)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
+ coderay (1.1.3)
23
+ concurrent-ruby (1.1.9)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.10)
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.4)
29
+ nxt_registry (0.3.10)
30
30
  activesupport
31
- pry (0.12.2)
32
- coderay (~> 1.1.0)
33
- method_source (~> 0.9.0)
31
+ pry (0.14.1)
32
+ coderay (~> 1.1)
33
+ method_source (~> 1.0)
34
34
  rake (12.3.3)
35
- rspec (3.9.0)
36
- rspec-core (~> 3.9.0)
37
- rspec-expectations (~> 3.9.0)
38
- rspec-mocks (~> 3.9.0)
39
- rspec-core (3.9.0)
40
- rspec-support (~> 3.9.0)
41
- rspec-expectations (3.9.0)
35
+ rexml (3.2.5)
36
+ rspec (3.10.0)
37
+ rspec-core (~> 3.10.0)
38
+ rspec-expectations (~> 3.10.0)
39
+ rspec-mocks (~> 3.10.0)
40
+ rspec-core (3.10.0)
41
+ rspec-support (~> 3.10.0)
42
+ rspec-expectations (3.10.0)
42
43
  diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.9.0)
44
- rspec-mocks (3.9.0)
44
+ rspec-support (~> 3.10.0)
45
+ rspec-mocks (3.10.0)
45
46
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.9.0)
47
- rspec-support (3.9.0)
47
+ rspec-support (~> 3.10.0)
48
+ rspec-support (3.10.0)
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
- thread_safe (0.3.6)
52
- tzinfo (1.2.6)
53
- thread_safe (~> 0.1)
54
- zeitwerk (2.2.2)
54
+ tzinfo (2.0.4)
55
+ concurrent-ruby (~> 1.0)
56
+ zeitwerk (2.4.2)
55
57
 
56
58
  PLATFORMS
57
59
  ruby
@@ -64,6 +66,7 @@ DEPENDENCIES
64
66
  rake (~> 12.0)
65
67
  rspec (~> 3.0)
66
68
  rspec_junit_formatter
69
+ ruby-graphviz
67
70
  sqlite3
68
71
 
69
72
  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
 
@@ -292,12 +298,27 @@ event :approve do
292
298
 
293
299
  after_transition from: %i[written submitted deleted], to: :approved, run: :call_me_back
294
300
 
295
- around_transition from: any_state, to: :approved do |block|
301
+ around_transition from: any_state, to: :approved do |block, _transition|
296
302
  # Note that around transition callbacks get passed a proc object that you have to call
297
303
  puts 'around transition enter'
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
312
+ end
313
+ ```
314
+
315
+ In callbacks you also have access to the current transition object. Through it you also have access to the arguments
316
+ and options that have been passed in when the transition was triggered:
317
+
318
+ ```ruby
319
+ before_transition from: any_state, to: :processed do |transition|
320
+ puts transition.arguments # => :arg_1, :arg_2 what was passed to the process!(:arg_1, :arg_2)
321
+ puts transition.options # => { arg_1: 'arg 1', arg_2: 'arg 2' } what was passed to the process!(arg_1: 'arg 1', arg_2: 'arg 2')
301
322
  end
302
323
  ```
303
324
 
@@ -333,7 +354,10 @@ You want to break out of your transition (which is wrapped inside a lock)?
333
354
  You can raise an error, have everything rolled back and then have your error handler take over.
334
355
  **NOTE:** Unless you reload your model all assignments you did, previous to the error, should still be available in your
335
356
  error handler. You can also defuse errors. This means they will not cause a rollback of the transaction during the
336
- transition and you can actually persist changes to your model before the defused error is raised and handled.
357
+ transition and you can actually persist changes to your model before the defused error is raised and handled. You can
358
+ also switch off locking (and transactions) for events by passing the `lock_transitions: false` option when defining an event. This
359
+ can also by set globally for a state_machine by passing the `lock_transitions: false` option when setting up the state
360
+ machine.
337
361
 
338
362
  ```ruby
339
363
  state_machine do
@@ -351,6 +375,14 @@ state_machine do
351
375
  raise CustomError, 'This does not rollback the headline update above'
352
376
  end
353
377
  end
378
+
379
+ event :approve_without_lock, lock_transitions: false do
380
+ transition from: %i[written submitted deleted], to: :approved do |headline:|
381
+ # This will be saved to the database because the event does not wrap the transition in a transaction
382
+ article.update!(headline: headline)
383
+ raise StandardError, 'This does not rollback the headline update above'
384
+ end
385
+ end
354
386
 
355
387
  on_error! CustomError from: any_state, to: :approved do |error, transition|
356
388
  # You can still handle the defused Error if you want to
@@ -384,14 +416,6 @@ class Article < ApplicationRecord
384
416
  end
385
417
  ```
386
418
 
387
-
388
- ## TODO
389
- - Decide on how to include state methods in model classes
390
- - Should we clone machines for each context?
391
- - What about inheritance? => What would be the expected behaviour? (dup vs. no dup)
392
- => Might also make sense to walk the ancestors chain and collect configure blocks
393
-
394
-
395
419
  ## Development
396
420
 
397
421
  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
@@ -25,19 +25,19 @@ require "nxt_state_machine/transition/interface"
25
25
  require "nxt_state_machine/transition"
26
26
  require "nxt_state_machine/transition/factory"
27
27
  require "nxt_state_machine/transition/proxy"
28
- require "nxt_state_machine/transition/store"
29
28
  require "nxt_state_machine/transition/around_callback_chain"
30
29
  require "nxt_state_machine/state_machine"
31
30
  require "nxt_state_machine/integrations/active_record"
32
31
  require "nxt_state_machine/integrations/attr_accessor"
33
32
  require "nxt_state_machine/integrations/hash"
33
+ require "nxt_state_machine/graph"
34
34
 
35
35
  module NxtStateMachine
36
36
  module ClassMethods
37
37
  include NxtRegistry
38
38
 
39
39
  def state_machine(name = :default, **opts, &config)
40
- state_machines.resolve!(name) || state_machines.register(
40
+ state_machines.resolve(name) || state_machines.register(
41
41
  name,
42
42
  StateMachine.new(name, self, state_machine_event_registry, **opts).configure(&config)
43
43
  )
@@ -75,15 +75,15 @@ module NxtStateMachine
75
75
  end
76
76
 
77
77
  def state_machine(name = :default)
78
- @state_machine ||= self.class.state_machines.resolve(name)
78
+ @state_machine ||= self.class.state_machines.resolve!(name)
79
79
  end
80
80
 
81
81
  def current_state_name(name = :default)
82
- state_machines.resolve(name).current_state_name(self)
82
+ state_machines.resolve!(name).current_state_name(self)
83
83
  end
84
84
 
85
85
  def current_state(name = :default)
86
- state_machines.resolve(name).states.resolve(current_state_name(name))
86
+ state_machines.resolve!(name).states.resolve!(current_state_name(name))
87
87
  end
88
88
 
89
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
@@ -11,15 +11,15 @@ module NxtStateMachine
11
11
  end
12
12
  end
13
13
 
14
- def resolve(transition)
15
- errors.from(transition.from.enum).to(transition.to.enum)
14
+ def resolve!(transition)
15
+ errors.from!(transition.from.enum).to!(transition.to.enum)
16
16
  end
17
17
 
18
18
  private
19
19
 
20
20
  def errors
21
21
  @errors ||= registry :from do
22
- nested :to, default: -> { [] }
22
+ level :to, default: -> { [] }
23
23
  end
24
24
  end
25
25
  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,17 +7,19 @@ 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,
@@ -37,7 +39,11 @@ module NxtStateMachine
37
39
  alias_method :transition, :transitions
38
40
 
39
41
  def transitions_from?(state)
40
- event_transitions.resolve!(state).present?
42
+ event_transitions.resolve(state).present?
43
+ end
44
+
45
+ def to_s
46
+ "#{self.class.name}[:#{name}]"
41
47
  end
42
48
 
43
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
 
@@ -49,14 +50,14 @@ module NxtStateMachine
49
50
  result = nil
50
51
  defused_error = nil
51
52
 
52
- target.with_lock do
53
+ with_conditional_lock(target, transition.event) do
53
54
  transition.run_before_callbacks
54
55
  result = execute_transition(target, transition, state_attr, save_with_method)
55
56
  transition.run_after_callbacks
56
57
 
57
58
  result
58
59
  rescue StandardError => error
59
- if machine.defuse_registry.resolve(transition).find { |error_class| error.is_a?(error_class) }
60
+ if machine.defuse_registry.resolve!(transition).find { |error_class| error.is_a?(error_class) }
60
61
  defused_error = error
61
62
  else
62
63
  raise error
@@ -65,7 +66,7 @@ module NxtStateMachine
65
66
 
66
67
  raise defused_error if defused_error
67
68
 
68
- result
69
+ transition.run_success_callbacks || result
69
70
  rescue StandardError => error
70
71
  target.assign_attributes(state_attr => transition.from.to_s)
71
72
 
@@ -82,6 +83,16 @@ module NxtStateMachine
82
83
  block ? result : set_state_result
83
84
  end
84
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
85
96
  end
86
97
 
87
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,7 +6,7 @@ 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
@@ -15,7 +15,7 @@ module NxtStateMachine
15
15
  @initial_state = nil
16
16
  end
17
17
 
18
- attr_reader :class_context, :transitions, :events, :options, :callbacks, :name, :error_callback_registry, :defuse_registry
18
+ attr_reader :class_context, :events, :options, :callbacks, :name, :error_callback_registry, :defuse_registry
19
19
  attr_accessor :initial_state
20
20
 
21
21
  def get_state_with(method = nil, &block)
@@ -89,8 +89,9 @@ module NxtStateMachine
89
89
  Event::Names.set_state_method_map(name).each do |event_name, set_state_method|
90
90
  class_context.define_method event_name do |*args, **opts|
91
91
  event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
92
- transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
93
- 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)
94
95
  end
95
96
  end
96
97
 
@@ -100,7 +101,7 @@ module NxtStateMachine
100
101
  end
101
102
 
102
103
  def can_transition?(event_name, from)
103
- event = events.resolve(event_name)
104
+ event = events.resolve!(event_name)
104
105
  event && event.event_transitions.key?(from)
105
106
  end
106
107
 
@@ -117,6 +118,10 @@ module NxtStateMachine
117
118
  callbacks.register(from, to, :after, run, block)
118
119
  end
119
120
 
121
+ def on_success(from:, to:, run: nil, &block)
122
+ callbacks.register(from, to, :success, run, block)
123
+ end
124
+
120
125
  def defuse(errors = [], from:, to:)
121
126
  defuse_registry.register(from, to, errors)
122
127
  end
@@ -147,16 +152,23 @@ module NxtStateMachine
147
152
  run_callbacks(transition, :after, context)
148
153
  end
149
154
 
155
+ def run_success_callbacks(transition, context)
156
+ run_callbacks(transition, :success, context)
157
+ end
158
+
150
159
  def find_error_callback(error, transition)
151
160
  error_callback_registry.resolve(error, transition)
152
161
  end
153
162
 
154
163
  def run_callbacks(transition, kind, context)
155
- current_callbacks = callbacks.resolve(transition, kind)
164
+ current_callbacks = callbacks.resolve!(transition, kind)
165
+ return unless current_callbacks.any?
156
166
 
157
167
  current_callbacks.each do |callback|
158
168
  Callable.new(callback).bind(context).call(transition)
159
169
  end
170
+
171
+ true
160
172
  end
161
173
 
162
174
  def current_state_name(context)
@@ -2,18 +2,21 @@ module NxtStateMachine
2
2
  class Transition
3
3
  include Transition::Interface
4
4
 
5
- def initialize(name, event:, from:, to:, state_machine:, context:, set_state_method:, &block)
5
+ def initialize(name, event:, from:, to:, state_machine:, context:, set_state_method:, arguments:, options:, &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
+ @arguments = arguments
15
+ @options = options
16
+ @result = nil
14
17
  end
15
18
 
16
- attr_reader :name, :from, :to, :block, :event
19
+ attr_reader :name, :from, :to, :block, :event, :result, :arguments, :options
17
20
 
18
21
  # This triggers the set state method
19
22
  def trigger
@@ -31,7 +34,7 @@ module NxtStateMachine
31
34
 
32
35
  # This must be used in set_state method to actually execute the transition within the around callback chain
33
36
  def execute(&block)
34
- Transition::Proxy.new(event, state_machine,self, context).call(&block)
37
+ self.result = Transition::Proxy.new(event, state_machine,self, context).call(&block)
35
38
  end
36
39
 
37
40
  alias_method :with_around_callbacks, :execute
@@ -44,9 +47,13 @@ module NxtStateMachine
44
47
  state_machine.run_after_callbacks(self, context)
45
48
  end
46
49
 
50
+ def run_success_callbacks
51
+ state_machine.run_success_callbacks(self, context)
52
+ end
53
+
47
54
  private
48
55
 
49
56
  attr_reader :state_machine, :set_state_method, :context
50
- attr_writer :block
57
+ attr_writer :block, :result
51
58
  end
52
59
  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
 
@@ -25,16 +25,22 @@ module NxtStateMachine
25
25
  state_machine: state_machine,
26
26
  context: context,
27
27
  event: event,
28
- set_state_method: set_state_method
28
+ set_state_method: set_state_method,
29
+ arguments: args,
30
+ options: opts
29
31
  }
30
32
 
31
- transition = Transition.new(name, **options)
33
+ transition = Transition.new(event.name, **options)
32
34
 
33
35
  if block
34
36
  # if the transition takes a block we make it available through a proxy on the transition itself!
35
37
  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
+ # if the transition block takes arguments we always pass the transition itself as the first argument
39
+ # callbacks also get passed the transition object in case they take an argument and can access args and
40
+ # options passed to the transition when invoked through that transition object
41
+ if block.arity > 0
42
+ args = [transition] + args
43
+ end
38
44
  context.instance_exec(*args, **opts, &block)
39
45
  end)
40
46
  end
@@ -1,3 +1,3 @@
1
1
  module NxtStateMachine
2
- VERSION = "0.1.6"
2
+ VERSION = '0.1.10'
3
3
  end
data/lib/railtie.rb ADDED
@@ -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,7 +35,7 @@ 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
41
  spec.add_development_dependency "rake", "~> 12.0"
@@ -43,4 +43,5 @@ Gem::Specification.new do |spec|
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
data/state_machine.png ADDED
Binary file
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nxt_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Robecke
8
8
  - Nils Sommer
9
9
  - Raphael Kallensee
10
10
  - Lütfi Demirci
11
- autorequire:
11
+ autorequire:
12
12
  bindir: exe
13
13
  cert_chain: []
14
- date: 2020-03-02 00:00:00.000000000 Z
14
+ date: 2021-07-19 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
@@ -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
@@ -166,6 +181,7 @@ files:
166
181
  - lib/nxt_state_machine/event.rb
167
182
  - lib/nxt_state_machine/event/names.rb
168
183
  - lib/nxt_state_machine/event_registry.rb
184
+ - lib/nxt_state_machine/graph.rb
169
185
  - lib/nxt_state_machine/integrations/active_record.rb
170
186
  - lib/nxt_state_machine/integrations/attr_accessor.rb
171
187
  - lib/nxt_state_machine/integrations/hash.rb
@@ -177,9 +193,11 @@ files:
177
193
  - lib/nxt_state_machine/transition/factory.rb
178
194
  - lib/nxt_state_machine/transition/interface.rb
179
195
  - lib/nxt_state_machine/transition/proxy.rb
180
- - lib/nxt_state_machine/transition/store.rb
181
196
  - lib/nxt_state_machine/version.rb
197
+ - lib/railtie.rb
198
+ - lib/tasks/draw_graph.rake
182
199
  - nxt_state_machine.gemspec
200
+ - state_machine.png
183
201
  homepage: https://github.com/nxt-insurance/nxt_state_machine
184
202
  licenses:
185
203
  - MIT
@@ -187,7 +205,7 @@ metadata:
187
205
  allowed_push_host: https://rubygems.org
188
206
  homepage_uri: https://github.com/nxt-insurance/nxt_state_machine
189
207
  source_code_uri: https://github.com/nxt-insurance/nxt_state_machine
190
- post_install_message:
208
+ post_install_message:
191
209
  rdoc_options: []
192
210
  require_paths:
193
211
  - lib
@@ -202,8 +220,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
220
  - !ruby/object:Gem::Version
203
221
  version: '0'
204
222
  requirements: []
205
- rubygems_version: 3.0.3
206
- signing_key:
223
+ rubygems_version: 3.1.6
224
+ signing_key:
207
225
  specification_version: 4
208
226
  summary: A rich but straight forward state machine library
209
227
  test_files: []
@@ -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