rails-workflow 1.4.5.4 → 1.4.6.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|