rails-workflow 1.4.5.4 → 1.4.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +23 -0
- data/Gemfile +2 -1
- data/Rakefile +4 -4
- data/bin/console +3 -3
- data/lib/active_support/overloads.rb +13 -6
- data/lib/workflow.rb +12 -279
- data/lib/workflow/adapters/active_record.rb +57 -50
- data/lib/workflow/adapters/active_record_validations.rb +25 -19
- data/lib/workflow/adapters/adapter.rb +23 -0
- data/lib/workflow/adapters/remodel.rb +8 -9
- data/lib/workflow/callbacks.rb +60 -45
- data/lib/workflow/callbacks/callback.rb +23 -37
- data/lib/workflow/callbacks/method_callback.rb +12 -0
- data/lib/workflow/callbacks/proc_callback.rb +23 -0
- data/lib/workflow/callbacks/string_callback.rb +12 -0
- data/lib/workflow/callbacks/transition_callback.rb +88 -78
- data/lib/workflow/callbacks/transition_callbacks/method_caller.rb +53 -0
- data/lib/workflow/callbacks/transition_callbacks/proc_caller.rb +60 -0
- data/lib/workflow/configuration.rb +1 -0
- data/lib/workflow/definition.rb +73 -0
- data/lib/workflow/errors.rb +37 -6
- data/lib/workflow/event.rb +30 -15
- data/lib/workflow/helper_method_configurator.rb +100 -0
- data/lib/workflow/specification.rb +45 -22
- data/lib/workflow/state.rb +45 -36
- data/lib/workflow/transition_context.rb +5 -4
- data/lib/workflow/transitions.rb +94 -0
- data/lib/workflow/version.rb +2 -1
- data/rails-workflow.gemspec +18 -18
- data/tags.markdown +31 -0
- metadata +13 -5
- data/lib/workflow/callbacks/transition_callbacks/method_wrapper.rb +0 -102
- data/lib/workflow/callbacks/transition_callbacks/proc_wrapper.rb +0 -48
- data/lib/workflow/draw.rb +0 -79
@@ -1,43 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Workflow
|
2
|
-
module
|
3
|
+
module Adapters
|
3
4
|
module ActiveRecord
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_validation :write_initial_state
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
11
|
+
def load_workflow_state
|
12
|
+
read_attribute(self.class.workflow_column)&.to_sym
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
update_columns attrs
|
25
|
-
new_value
|
26
|
-
else
|
27
|
-
self[self.class.workflow_column] = new_value
|
15
|
+
# On transition the new workflow state is immediately saved in the
|
16
|
+
# database, if configured to do so.
|
17
|
+
def persist_workflow_state(new_value)
|
18
|
+
# Rails 3.1 or newer
|
19
|
+
if persisted? && Workflow.config.persist_workflow_state_immediately
|
20
|
+
attrs = { self.class.workflow_column => new_value }
|
21
|
+
if Workflow.config.touch_on_update_column
|
22
|
+
attrs[:updated_at] = DateTime.now
|
28
23
|
end
|
24
|
+
update_columns attrs
|
25
|
+
new_value
|
26
|
+
else
|
27
|
+
self[self.class.workflow_column] = new_value
|
29
28
|
end
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def write_initial_state
|
39
|
-
write_attribute self.class.workflow_column, current_state.name
|
40
|
-
end
|
31
|
+
# Motivation: even if NULL is stored in the workflow_state database column,
|
32
|
+
# the current_state is correctly recognized in the Ruby code. The problem
|
33
|
+
# arises when you want to SELECT records filtering by the value of initial
|
34
|
+
# state. That's why it is important to save the string with the name of the
|
35
|
+
# initial state in all the new records.
|
36
|
+
private def write_initial_state
|
37
|
+
write_attribute self.class.workflow_column, current_state.name
|
41
38
|
end
|
42
39
|
|
43
40
|
# This module will automatically generate ActiveRecord scopes based on workflow states.
|
@@ -47,31 +44,41 @@ module Workflow
|
|
47
44
|
#
|
48
45
|
# Article.with_pending_state # => ActiveRecord::Relation
|
49
46
|
# Payment.without_refunded_state # => ActiveRecord::Relation
|
50
|
-
|
47
|
+
# `
|
51
48
|
# Example above just adds `where(:state_column_name => 'pending')` or
|
52
49
|
# `where.not(:state_column_name => 'pending')` to AR query and returns
|
53
50
|
# ActiveRecord::Relation.
|
54
|
-
module
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
module ClassMethods
|
52
|
+
# Instructs Workflow which column to use to persist workflow state.
|
53
|
+
#
|
54
|
+
# @param [Symbol] column_name If provided, will set a new workflow column name.
|
55
|
+
# @return [Symbol] The current (or new) name for the workflow column on this class.
|
56
|
+
def workflow_column(column_name = nil)
|
57
|
+
@workflow_column = column_name.to_sym if column_name
|
58
|
+
@workflow_column ||= superclass_workflow || :workflow_state
|
60
59
|
end
|
61
60
|
|
62
|
-
def
|
63
|
-
|
64
|
-
states
|
61
|
+
def workflow(&specification)
|
62
|
+
super
|
63
|
+
workflow_spec.states.each { |state| define_scopes(state) }
|
64
|
+
end
|
65
65
|
|
66
|
-
|
67
|
-
define_singleton_method("with_#{state}_state") do
|
68
|
-
where(self.workflow_column.to_sym => state.to_s)
|
69
|
-
end
|
66
|
+
private
|
70
67
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
def superclass_workflow
|
69
|
+
superclass.workflow_column if superclass.respond_to?(:workflow_column)
|
70
|
+
end
|
71
|
+
|
72
|
+
def define_scopes(state)
|
73
|
+
state_name = state.name
|
74
|
+
|
75
|
+
scope "with_#{state_name}_state", lambda {
|
76
|
+
where(workflow_column => state_name)
|
77
|
+
}
|
78
|
+
|
79
|
+
scope "without_#{state_name}_state", lambda {
|
80
|
+
where.not(workflow_column => state_name)
|
81
|
+
}
|
75
82
|
end
|
76
83
|
end
|
77
84
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'active_support/concern'
|
3
|
+
require 'English'
|
2
4
|
|
3
5
|
module Workflow
|
4
|
-
module
|
6
|
+
module Adapters
|
5
7
|
module ActiveRecordValidations
|
6
8
|
extend ActiveSupport::Concern
|
7
9
|
|
@@ -14,13 +16,14 @@ module Workflow
|
|
14
16
|
# Captures instance method calls of the form `:transitioning_from_<state_name>`
|
15
17
|
# and `:transitioning_to_<state_name>`.
|
16
18
|
#
|
17
|
-
# For use with validators, e.g. `validates :foobar, presence: true,
|
19
|
+
# For use with validators, e.g. `validates :foobar, presence: true,
|
20
|
+
# if: :transitioning_to_some_state?`
|
18
21
|
#
|
19
22
|
def method_missing(method, *args, &block)
|
20
23
|
if method.to_s =~ /^transitioning_(from|to|via_event)_([\w_-]+)\?$/
|
21
24
|
class_eval "
|
22
25
|
def #{method}
|
23
|
-
transitioning? direction: '#{
|
26
|
+
transitioning? direction: '#{$LAST_MATCH_INFO[1]}', state: '#{$LAST_MATCH_INFO[2]}'
|
24
27
|
end
|
25
28
|
"
|
26
29
|
send method
|
@@ -29,6 +32,10 @@ module Workflow
|
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
35
|
+
def respond_to_missing?(method_name, _include_private = false)
|
36
|
+
method_name.to_s =~ /^transitioning_(from|to|via_event)_([\w_-]+)\?$/
|
37
|
+
end
|
38
|
+
|
32
39
|
def can_transition?(event_id)
|
33
40
|
event = current_state.find_event(event_id)
|
34
41
|
return false unless event
|
@@ -51,7 +58,8 @@ module Workflow
|
|
51
58
|
# correct answers to the questions, `:transitioning_from_<old_state>?`.
|
52
59
|
# `:transitioning_to_<new_state>`, `:transitioning_via_event_<event_name>?`
|
53
60
|
#
|
54
|
-
# For use with validators, e.g. `validates :foobar, presence: true,
|
61
|
+
# For use with validators, e.g. `validates :foobar, presence: true,
|
62
|
+
# if: :transitioning_to_some_state?`
|
55
63
|
#
|
56
64
|
# = Example:
|
57
65
|
#
|
@@ -61,19 +69,17 @@ module Workflow
|
|
61
69
|
# end
|
62
70
|
# end
|
63
71
|
#
|
64
|
-
def within_transition(from, to, event
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
@transition_context = nil
|
76
|
-
end
|
72
|
+
def within_transition(from, to, event)
|
73
|
+
@transition_context = TransitionContext.new \
|
74
|
+
from: from,
|
75
|
+
to: to,
|
76
|
+
event: event,
|
77
|
+
attributes: {},
|
78
|
+
event_args: []
|
79
|
+
|
80
|
+
return yield
|
81
|
+
ensure
|
82
|
+
@transition_context = nil
|
77
83
|
end
|
78
84
|
|
79
85
|
# Override for ActiveRecord::Validations#valid?
|
@@ -89,7 +95,7 @@ module Workflow
|
|
89
95
|
# meaning that one should call `errors.clear` before {#valid?} will
|
90
96
|
# trigger validations to run anew.
|
91
97
|
module RevalidateOnlyAfterAttributesChange
|
92
|
-
def valid?(context=nil)
|
98
|
+
def valid?(context = nil)
|
93
99
|
if errors.any? && !@changed_since_validation
|
94
100
|
false
|
95
101
|
else
|
@@ -109,7 +115,7 @@ module Workflow
|
|
109
115
|
|
110
116
|
module ClassMethods
|
111
117
|
def halt_transition_unless_valid!
|
112
|
-
before_transition unless: :valid? do |
|
118
|
+
before_transition unless: :valid? do |_model|
|
113
119
|
throw :abort
|
114
120
|
end
|
115
121
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Workflow
|
3
|
+
module Adapters
|
4
|
+
module Adapter
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
# Look for a hook; otherwise detect based on ancestor class.
|
8
|
+
if respond_to?(:workflow_adapter)
|
9
|
+
include workflow_adapter
|
10
|
+
else
|
11
|
+
if Adapters.const_defined?(:ActiveRecord) && self < ::ActiveRecord::Base
|
12
|
+
include Adapters::ActiveRecord
|
13
|
+
include ActiveRecordValidations
|
14
|
+
end
|
15
|
+
|
16
|
+
if Object.const_defined?(:Remodel) && klass < Adapter::Remodel::Entity
|
17
|
+
include :Remodel
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,15 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Workflow
|
2
|
-
module
|
3
|
+
module Adapters
|
3
4
|
module Remodel
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
5
|
+
def load_workflow_state
|
6
|
+
send(self.class.workflow_column)
|
7
|
+
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
end
|
9
|
+
def persist_workflow_state(new_value)
|
10
|
+
update(self.class.workflow_column => new_value)
|
12
11
|
end
|
13
12
|
end
|
14
13
|
end
|
15
|
-
end
|
14
|
+
end
|
data/lib/workflow/callbacks.rb
CHANGED
@@ -1,68 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'workflow/callbacks/callback'
|
2
3
|
require 'workflow/callbacks/transition_callback'
|
3
|
-
require 'workflow/callbacks/transition_callbacks/
|
4
|
-
require 'workflow/callbacks/transition_callbacks/
|
4
|
+
require 'workflow/callbacks/transition_callbacks/method_caller'
|
5
|
+
require 'workflow/callbacks/transition_callbacks/proc_caller'
|
5
6
|
|
6
7
|
module Workflow
|
7
8
|
module Callbacks
|
8
|
-
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
include ActiveSupport::Callbacks
|
11
11
|
|
12
|
-
CALLBACK_MAP =
|
12
|
+
CALLBACK_MAP = {
|
13
13
|
transition: :event,
|
14
14
|
exit: :from,
|
15
15
|
enter: :to
|
16
16
|
}.freeze
|
17
17
|
|
18
|
-
# @!attribute [r] transition_context
|
19
|
-
# During state transition events, contains a {TransitionContext} representing the transition underway.
|
20
|
-
# @return [TransitionContext] representation of current state transition
|
21
|
-
#
|
22
18
|
included do
|
23
19
|
CALLBACK_MAP.keys.each do |type|
|
24
20
|
define_callbacks type,
|
25
|
-
|
21
|
+
skip_after_callbacks_if_terminated: true
|
26
22
|
end
|
27
23
|
end
|
28
24
|
|
29
25
|
module ClassMethods
|
30
|
-
def ensure_after_transitions(name=nil, **opts, &block)
|
31
|
-
|
32
|
-
Callback.
|
26
|
+
def ensure_after_transitions(name = nil, **opts, &block)
|
27
|
+
ensure_procs = [name, block].compact.map do |exe|
|
28
|
+
Callback.build(exe)
|
33
29
|
end
|
34
30
|
|
35
|
-
prepend_around_transition
|
31
|
+
prepend_around_transition(**opts) do |obj, callbacks|
|
36
32
|
begin
|
37
|
-
callbacks.call
|
33
|
+
callbacks.call
|
38
34
|
ensure
|
39
|
-
|
35
|
+
ensure_procs.each { |l| l.callback.call obj, -> {} }
|
40
36
|
end
|
41
37
|
end
|
42
38
|
end
|
43
39
|
|
44
|
-
|
45
|
-
_error_procs = [opts.delete(:rescue)].compact.map do |exe|
|
46
|
-
Callback.new(exe)
|
47
|
-
end
|
40
|
+
EMPTY_LAMBDA = -> {}
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
def on_error(error_class = Exception, **opts, &block)
|
43
|
+
error_procs = build_lambdas([opts.delete(:rescue), block])
|
44
|
+
ensure_procs = build_lambdas(opts.delete(:ensure))
|
45
|
+
|
46
|
+
prepend_around_transition(**opts, &on_error_proc(error_class, error_procs, ensure_procs))
|
47
|
+
end
|
52
48
|
|
53
|
-
|
49
|
+
private def on_error_proc(error_class, error_procs, ensure_procs)
|
50
|
+
proc do |_obj, callbacks|
|
54
51
|
begin
|
55
52
|
callbacks.call
|
56
53
|
rescue error_class => ex
|
57
|
-
|
58
|
-
|
59
|
-
_error_procs.each {|l| l.callback.call self, ->{}}
|
54
|
+
instance_exec(ex, &block) if block_given?
|
55
|
+
error_procs.each { |l| l.callback.call self, EMPTY_LAMBDA }
|
60
56
|
ensure
|
61
|
-
|
57
|
+
ensure_procs.each { |l| l.callback.call self, EMPTY_LAMBDA }
|
62
58
|
end
|
63
59
|
end
|
64
60
|
end
|
65
61
|
|
62
|
+
private def build_lambdas(*names)
|
63
|
+
[names].flatten.compact.map do |name|
|
64
|
+
Callback.build name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
66
68
|
##
|
67
69
|
# @!method before_transition
|
68
70
|
#
|
@@ -247,13 +249,13 @@ module Workflow
|
|
247
249
|
CALLBACK_MAP.each do |type, context_attribute|
|
248
250
|
define_method "#{callback}_#{type}" do |*names, &blk|
|
249
251
|
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
250
|
-
set_callback(type, callback,
|
252
|
+
set_callback(type, callback, cb(callback, name, self), options)
|
251
253
|
end
|
252
254
|
end
|
253
255
|
|
254
256
|
define_method "prepend_#{callback}_#{type}" do |*names, &blk|
|
255
257
|
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
256
|
-
set_callback(type, callback,
|
258
|
+
set_callback(type, callback, cb(callback, name, self), options.merge(prepend: true))
|
257
259
|
end
|
258
260
|
end
|
259
261
|
|
@@ -270,17 +272,19 @@ module Workflow
|
|
270
272
|
end
|
271
273
|
end
|
272
274
|
|
273
|
-
def
|
275
|
+
private def cb(callback, name, target)
|
276
|
+
Callbacks::TransitionCallback.build(callback, name, target)
|
277
|
+
end
|
278
|
+
|
279
|
+
def applicable_callback?(_callback, procedure)
|
274
280
|
arity = procedure.arity
|
275
|
-
return true if arity
|
276
|
-
|
277
|
-
|
278
|
-
else
|
279
|
-
return false
|
280
|
-
end
|
281
|
+
return true if arity.negative? || arity > 2
|
282
|
+
|
283
|
+
[:key, :keyreq, :keyrest].include?(procedure.parameters[-1][0])
|
281
284
|
end
|
282
285
|
|
283
286
|
private
|
287
|
+
|
284
288
|
def _insert_callbacks(callbacks, context_attribute, block = nil)
|
285
289
|
options = callbacks.extract_options!
|
286
290
|
_normalize_callback_options(options, context_attribute)
|
@@ -296,30 +300,41 @@ module Workflow
|
|
296
300
|
end
|
297
301
|
|
298
302
|
def _normalize_callback_option(options, context_attribute, from, to) # :nodoc:
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
options[to] = Array(options[to]).unshift(from)
|
303
|
+
return unless options[from]
|
304
|
+
|
305
|
+
all_names = Array(options[from]).map(&:to_sym).to_set
|
306
|
+
from = proc do |record|
|
307
|
+
all_names.include? record.transition_context.send(context_attribute).to_sym
|
305
308
|
end
|
309
|
+
options[to] = Array(options[to]).unshift(from)
|
306
310
|
end
|
307
311
|
end
|
308
312
|
|
309
313
|
private
|
314
|
+
|
310
315
|
# TODO: Do something here.
|
311
316
|
def halted_callback_hook(filter)
|
312
317
|
# byebug
|
313
318
|
end
|
314
319
|
|
315
|
-
def run_all_callbacks
|
320
|
+
def run_all_callbacks
|
316
321
|
catch(:abort) do
|
317
322
|
run_callbacks :transition do
|
318
|
-
|
319
|
-
|
323
|
+
aborting_callbacks(:exit) do
|
324
|
+
aborting_callbacks(:enter) do
|
325
|
+
yield
|
326
|
+
end
|
320
327
|
end
|
321
328
|
end
|
322
329
|
end
|
323
330
|
end
|
331
|
+
|
332
|
+
def aborting_callbacks(kind)
|
333
|
+
return_value = run_callbacks(kind) do
|
334
|
+
yield
|
335
|
+
end
|
336
|
+
throw(:abort) if false == return_value
|
337
|
+
return_value
|
338
|
+
end
|
324
339
|
end
|
325
340
|
end
|