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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +23 -0
  4. data/Gemfile +2 -1
  5. data/Rakefile +4 -4
  6. data/bin/console +3 -3
  7. data/lib/active_support/overloads.rb +13 -6
  8. data/lib/workflow.rb +12 -279
  9. data/lib/workflow/adapters/active_record.rb +57 -50
  10. data/lib/workflow/adapters/active_record_validations.rb +25 -19
  11. data/lib/workflow/adapters/adapter.rb +23 -0
  12. data/lib/workflow/adapters/remodel.rb +8 -9
  13. data/lib/workflow/callbacks.rb +60 -45
  14. data/lib/workflow/callbacks/callback.rb +23 -37
  15. data/lib/workflow/callbacks/method_callback.rb +12 -0
  16. data/lib/workflow/callbacks/proc_callback.rb +23 -0
  17. data/lib/workflow/callbacks/string_callback.rb +12 -0
  18. data/lib/workflow/callbacks/transition_callback.rb +88 -78
  19. data/lib/workflow/callbacks/transition_callbacks/method_caller.rb +53 -0
  20. data/lib/workflow/callbacks/transition_callbacks/proc_caller.rb +60 -0
  21. data/lib/workflow/configuration.rb +1 -0
  22. data/lib/workflow/definition.rb +73 -0
  23. data/lib/workflow/errors.rb +37 -6
  24. data/lib/workflow/event.rb +30 -15
  25. data/lib/workflow/helper_method_configurator.rb +100 -0
  26. data/lib/workflow/specification.rb +45 -22
  27. data/lib/workflow/state.rb +45 -36
  28. data/lib/workflow/transition_context.rb +5 -4
  29. data/lib/workflow/transitions.rb +94 -0
  30. data/lib/workflow/version.rb +2 -1
  31. data/rails-workflow.gemspec +18 -18
  32. data/tags.markdown +31 -0
  33. metadata +13 -5
  34. data/lib/workflow/callbacks/transition_callbacks/method_wrapper.rb +0 -102
  35. data/lib/workflow/callbacks/transition_callbacks/proc_wrapper.rb +0 -48
  36. data/lib/workflow/draw.rb +0 -79
@@ -1,43 +1,40 @@
1
+ # frozen_string_literal: true
1
2
  module Workflow
2
- module Adapter
3
+ module Adapters
3
4
  module ActiveRecord
4
- def self.included(klass)
5
- klass.send :include, Adapter::ActiveRecord::InstanceMethods
6
- klass.send :extend, Adapter::ActiveRecord::Scopes
7
- klass.before_validation :write_initial_state
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_validation :write_initial_state
8
9
  end
9
10
 
10
- module InstanceMethods
11
- def load_workflow_state
12
- read_attribute(self.class.workflow_column)&.to_sym
13
- end
11
+ def load_workflow_state
12
+ read_attribute(self.class.workflow_column)&.to_sym
13
+ end
14
14
 
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
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
- private
32
-
33
- # Motivation: even if NULL is stored in the workflow_state database column,
34
- # the current_state is correctly recognized in the Ruby code. The problem
35
- # arises when you want to SELECT records filtering by the value of initial
36
- # state. That's why it is important to save the string with the name of the
37
- # initial state in all the new records.
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 Scopes
55
- def self.extended(object)
56
- class << object
57
- alias_method :workflow_without_scopes, :workflow unless method_defined?(:workflow_without_scopes)
58
- alias_method :workflow, :workflow_with_scopes
59
- end
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 workflow_with_scopes(&specification)
63
- workflow_without_scopes(&specification)
64
- states = workflow_spec.states
61
+ def workflow(&specification)
62
+ super
63
+ workflow_spec.states.each { |state| define_scopes(state) }
64
+ end
65
65
 
66
- states.map(&:name).each do |state|
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
- define_singleton_method("without_#{state}_state") do
72
- where.not(self.workflow_column.to_sym => state.to_s)
73
- end
74
- end
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 Adapter
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, if: :transitioning_to_some_state?`
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: '#{$~[1]}', state: '#{$~[2]}'
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, if: :transitioning_to_some_state?`
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, &block)
65
- begin
66
- @transition_context = TransitionContext.new \
67
- from: from,
68
- to: to,
69
- event: event,
70
- attributes: {},
71
- event_args: []
72
-
73
- return block.call()
74
- ensure
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 |model|
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 Adapter
3
+ module Adapters
3
4
  module Remodel
4
- module InstanceMethods
5
- def load_workflow_state
6
- send(self.class.workflow_column)
7
- end
5
+ def load_workflow_state
6
+ send(self.class.workflow_column)
7
+ end
8
8
 
9
- def persist_workflow_state(new_value)
10
- update(self.class.workflow_column => new_value)
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
@@ -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/method_wrapper'
4
- require 'workflow/callbacks/transition_callbacks/proc_wrapper'
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
- skip_after_callbacks_if_terminated: true
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
- _ensure_procs = [name, block].compact.map do |exe|
32
- Callback.new(exe)
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 **opts do |obj, callbacks|
31
+ prepend_around_transition(**opts) do |obj, callbacks|
36
32
  begin
37
- callbacks.call()
33
+ callbacks.call
38
34
  ensure
39
- _ensure_procs.each {|l| l.callback.call obj, ->{}}
35
+ ensure_procs.each { |l| l.callback.call obj, -> {} }
40
36
  end
41
37
  end
42
38
  end
43
39
 
44
- def on_error(error_class=Exception, **opts, &block)
45
- _error_procs = [opts.delete(:rescue)].compact.map do |exe|
46
- Callback.new(exe)
47
- end
40
+ EMPTY_LAMBDA = -> {}
48
41
 
49
- _ensure_procs = [opts.delete(:ensure)].compact.map do |exe|
50
- Callback.new(exe)
51
- end
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
- prepend_around_transition **opts do |obj, callbacks|
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
- self.instance_exec(ex, &block) if block_given?
58
- # block.call(ex) if block_given?
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
- _ensure_procs.each {|l| l.callback.call self, ->{}}
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, Callbacks::TransitionCallback.build_wrapper(callback, name, self), options)
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, Callbacks::TransitionCallback.build_wrapper(callback, name, self), options.merge(prepend: true))
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 applicable_callback?(callback, procedure)
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 < 0 || arity > 2
276
- if [:key, :keyreq, :keyrest].include? procedure.parameters[-1][0]
277
- return true
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
- if from = options[from]
300
- _from = Array(from).map(&:to_sym).to_set
301
- from = proc { |record|
302
- _from.include? record.transition_context.send(context_attribute).to_sym
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(&block)
320
+ def run_all_callbacks
316
321
  catch(:abort) do
317
322
  run_callbacks :transition do
318
- throw(:abort) if false == run_callbacks(:exit) do
319
- throw(:abort) if false == run_callbacks(:enter, &block)
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