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