rails-workflow 1.4.4.4 → 1.4.5.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.
@@ -0,0 +1,32 @@
1
+ require 'active_support/callbacks'
2
+
3
+ module ActiveSupport
4
+ # Overloads for {ActiveSupport::Callbacks::Callback} so it can recognize
5
+ # {Workflow::Callbacks::TransitionCallback}, which is duck-type equivalent
6
+ # to a lambda for the present purposes.
7
+ module CallbackOverloads
8
+ private
9
+ def make_lambda(filter)
10
+ if filter.kind_of? Workflow::Callbacks::TransitionCallback
11
+ super(filter.wrapper)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def compute_identifier(filter)
18
+ if filter.kind_of? Workflow::Callbacks::TransitionCallback
19
+ super(filter.raw_proc)
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Overload {ActiveSupport::Callbacks::Callback} with methods from {ActiveSupport::CallbackOverloads}.
28
+ # {Workflow::Callbacks::TransitionCallback}, which is duck-type equivalent
29
+ # to a lambda for the present purposes.
30
+ class ::ActiveSupport::Callbacks::Callback
31
+ prepend ActiveSupport::CallbackOverloads
32
+ end
data/lib/workflow.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'active_support/concern'
3
+ require 'active_support/callbacks'
3
4
  require 'workflow/version'
4
5
  require 'workflow/configuration'
5
6
  require 'workflow/specification'
@@ -8,6 +9,8 @@ require 'workflow/adapters/active_record'
8
9
  require 'workflow/adapters/remodel'
9
10
  require 'workflow/adapters/active_record_validations'
10
11
  require 'workflow/transition_context'
12
+ require 'active_support/overloads'
13
+
11
14
 
12
15
  # See also README.markdown for documentation
13
16
  module Workflow
@@ -18,35 +21,16 @@ module Workflow
18
21
  include Callbacks
19
22
  include Errors
20
23
 
21
- module ComputeIdentifier
22
- private
23
- def make_lambda(filter)
24
- if filter.kind_of? Workflow::Callbacks::TransitionCallbackWrapper
25
- super(filter.wrapper)
26
- else
27
- super
28
- end
29
- end
30
-
31
- def compute_identifier(filter)
32
- if filter.kind_of? Workflow::Callbacks::TransitionCallbackWrapper
33
- super(filter.raw_proc)
34
- else
35
- super
36
- end
37
- end
38
- end
39
-
40
- class ::ActiveSupport::Callbacks::Callback
41
- prepend ComputeIdentifier
42
- end
24
+ # The application-wide Workflow configuration object
25
+ CONFIGURATION = Configuration.new
43
26
 
44
- def self.configure(&block)
45
- block.call(config) if block_given?
46
- end
47
-
48
- def self.config
49
- @@configuration ||= Configuration.new
27
+ # Helper method for setting configuration options on {Workflow.config}
28
+ #
29
+ # @yield [Workflow::Configuration] config {Configuration} object to be manipulated.
30
+ # @return [nil]
31
+ def self.config(&block)
32
+ block.call(CONFIGURATION) if block_given?
33
+ return CONFIGURATION
50
34
  end
51
35
 
52
36
  included do
@@ -75,7 +59,7 @@ module Workflow
75
59
  end
76
60
 
77
61
  # Deprecated. Check for false return value from {#transition!}
78
- # @return true if the last transition was halted by one of the transition callbacks.
62
+ # @return [Boolean] true if the last transition was halted by one of the transition callbacks.
79
63
  def halted?
80
64
  @halted
81
65
  end
@@ -84,11 +68,21 @@ module Workflow
84
68
  # @return [String] The reason the transition was aborted.
85
69
  attr_reader :halted_because
86
70
 
71
+ # @api private
72
+ # @return [Workflow::TransitionContext] During transition, or nil if no transition is underway.
73
+ # During a state transition, contains transition-specific information:
74
+ # * The name of the {Workflow::State} being exited,
75
+ # * The name of the {Workflow::State} being entered,
76
+ # * The name of the {Workflow::Event} that was fired,
77
+ # * And whatever arguments were passed to the {Workflow#transition!} method.
78
+ attr_reader :transition_context
79
+
87
80
  # Initiates state transition via the named event
88
81
  #
89
82
  # @param [Symbol] name name of event to initiate
90
- # @param [Mixed] *args Arguments passed to state transition. Available also to callbacks
91
- # @return [Type] description of returned object
83
+ # @param [Array] args State transition arguments.
84
+ # @return [Symbol] The name of the new state, or `false` if the transition failed.
85
+ # TODO: connect args to documentation on how arguments are accessed during state transitions.
92
86
  def transition!(name, *args, **attributes)
93
87
  name = name.to_sym
94
88
  event = current_state.find_event(name)
@@ -128,8 +122,8 @@ module Workflow
128
122
 
129
123
  # Stop the current transition and set the reason for the abort.
130
124
  #
131
- # @param optional [String] reason Reason for halting transition.
132
- # @return [void]
125
+ # @param [String] reason Optional reason for halting transition.
126
+ # @return [nil]
133
127
  def halt(reason = nil)
134
128
  @halted_because = reason
135
129
  @halted = true
@@ -138,8 +132,8 @@ module Workflow
138
132
 
139
133
  # Sets halt reason and raises [TransitionHaltedError] error.
140
134
  #
141
- # @param optional [String] reason Reason for halting
142
- # @return [void]
135
+ # @param [String] reason Optional reason for halting
136
+ # @return [nil]
143
137
  def halt!(reason = nil)
144
138
  @halted_because = reason
145
139
  @halted = true
@@ -214,8 +208,8 @@ module Workflow
214
208
 
215
209
  # Instructs Workflow which column to use to persist workflow state.
216
210
  #
217
- # @param optional [Symbol] column_name name of column on table
218
- # @return [void]
211
+ # @param [Symbol] column_name If provided, will set a new workflow column name.
212
+ # @return [Symbol] The current (or new) name for the workflow column on this class.
219
213
  def workflow_column(column_name=nil)
220
214
  if column_name
221
215
  @workflow_state_column_name = column_name.to_sym
@@ -1,6 +1,7 @@
1
1
  require 'workflow/callbacks/callback'
2
- require 'workflow/callbacks/transition_callback_wrapper'
3
- require 'workflow/callbacks/transition_callback_method_wrapper'
2
+ require 'workflow/callbacks/transition_callback'
3
+ require 'workflow/callbacks/transition_callbacks/method_wrapper'
4
+ require 'workflow/callbacks/transition_callbacks/proc_wrapper'
4
5
 
5
6
  module Workflow
6
7
  module Callbacks
@@ -19,7 +20,6 @@ module Workflow
19
20
  # @return [TransitionContext] representation of current state transition
20
21
  #
21
22
  included do
22
- attr_reader :transition_context
23
23
  CALLBACK_MAP.keys.each do |type|
24
24
  define_callbacks type,
25
25
  skip_after_callbacks_if_terminated: true
@@ -247,13 +247,13 @@ module Workflow
247
247
  CALLBACK_MAP.each do |type, context_attribute|
248
248
  define_method "#{callback}_#{type}" do |*names, &blk|
249
249
  _insert_callbacks(names, context_attribute, blk) do |name, options|
250
- set_callback(type, callback, TransitionCallbackWrapper.build_wrapper(callback, name, self), options)
250
+ set_callback(type, callback, Callbacks::TransitionCallback.build_wrapper(callback, name, self), options)
251
251
  end
252
252
  end
253
253
 
254
254
  define_method "prepend_#{callback}_#{type}" do |*names, &blk|
255
255
  _insert_callbacks(names, context_attribute, blk) do |name, options|
256
- set_callback(type, callback, TransitionCallbackWrapper.build_wrapper(callback, name, self), options.merge(prepend: true))
256
+ set_callback(type, callback, Callbacks::TransitionCallback.build_wrapper(callback, name, self), options.merge(prepend: true))
257
257
  end
258
258
  end
259
259
 
@@ -1,8 +1,8 @@
1
1
 
2
2
  module Workflow
3
3
  module Callbacks
4
- class TransitionCallbackWrapper
5
- attr_reader :callback_type, :raw_proc
4
+ class TransitionCallback
5
+ attr_reader :callback_type, :raw_proc, :calling_class
6
6
  def initialize(callback_type, raw_proc, calling_class)
7
7
  @callback_type = callback_type
8
8
  @raw_proc = raw_proc
@@ -11,29 +11,34 @@ module Workflow
11
11
 
12
12
  def self.build_wrapper(callback_type, raw_proc, calling_class)
13
13
  if raw_proc.kind_of? ::Proc
14
- new(callback_type, raw_proc, calling_class).wrapper
14
+ TransitionCallbacks::ProcWrapper.new(callback_type, raw_proc, calling_class)
15
15
  elsif raw_proc.kind_of? ::Symbol
16
- TransitionCallbackMethodWrapper.new(callback_type, raw_proc, calling_class)
16
+ if zero_arity_method?(raw_proc, calling_class)
17
+ raw_proc
18
+ else
19
+ TransitionCallbacks::MethodWrapper.new(callback_type, raw_proc, calling_class)
20
+ end
17
21
  else
18
22
  raw_proc
19
23
  end
20
24
  end
21
25
 
22
26
  def wrapper
23
- arguments = [
24
- name_arguments_string,
25
- rest_param_string,
26
- kw_arguments_string,
27
- keyrest_string,
28
- procedure_string].compact.join(', ')
29
-
30
- raw_proc = self.raw_proc
31
- str = build_proc("target.instance_exec(#{arguments})")
32
- eval(str)
27
+ raise NotImplementedError.new "Abstract Method Called"
33
28
  end
34
29
 
35
30
  protected
36
31
 
32
+ # Returns true if the method has arity zero.
33
+ # Returns false if the method is defined and has non-zero arity.
34
+ # Returns nil if the method has not been defined.
35
+ def self.zero_arity_method?(method, calling_class)
36
+ if calling_class.instance_methods.include?(method)
37
+ method = calling_class.instance_method(method)
38
+ method.arity == 0
39
+ end
40
+ end
41
+
37
42
  def build_proc(proc_body)
38
43
  <<-EOF
39
44
  Proc.new do |target#{', callbacks' if around_callback?}|
@@ -56,12 +61,7 @@ module Workflow
56
61
  end
57
62
 
58
63
  def name_arguments_string
59
- params = name_params
60
- names = []
61
- names << 'target' if params.shift
62
- (names << 'callbacks') && params.shift if around_callback?
63
- names += params.map{|name| "name_proc.call(:#{name})"}
64
- return names.join(', ') if names.any?
64
+ raise NotImplementedError.new "Abstract Method Called"
65
65
  end
66
66
 
67
67
  def kw_arguments_string
@@ -80,7 +80,7 @@ module Workflow
80
80
  end
81
81
 
82
82
  def procedure_string
83
- '&raw_proc'
83
+ raise NotImplementedError.new "Abstract Method Called"
84
84
  end
85
85
 
86
86
  def name_params
@@ -106,11 +106,11 @@ module Workflow
106
106
  end
107
107
 
108
108
  def parameters
109
- raw_proc.parameters
109
+ raise NotImplementedError.new "Abstract Method Called"
110
110
  end
111
111
 
112
112
  def arity
113
- raw_proc.arity
113
+ raise NotImplementedError.new "Abstract Method Called"
114
114
  end
115
115
  end
116
116
  end
@@ -0,0 +1,102 @@
1
+ module Workflow
2
+ module Callbacks
3
+ module TransitionCallbacks
4
+ # A {Workflow::Callbacks::TransitionCallback} that wraps an instance method
5
+ # With arity != 0.
6
+ # Because the wrapped method may not have been defined at the time the callback
7
+ # is defined, the string representing the method call is built at runtime
8
+ # rather than at compile time.
9
+ class MethodWrapper < ::Workflow::Callbacks::TransitionCallback
10
+ attr_reader :calling_class
11
+
12
+ # Builds a proc object that will correctly call the {#raw_proc}
13
+ # by inspecting its parameters and pulling arguments from the {Workflow::TransitionContext}
14
+ # object for the transition.
15
+ # Given an overloaded `==` operator so for {Workflow#skip_before_transition} and other
16
+ # `skip_transition` calls.
17
+ # @return [Type] description of returned object
18
+ def wrapper
19
+ cb_object = self
20
+ proc_string = build_proc(<<-EOF)
21
+ arguments = [
22
+ cb_object.send(:raw_proc).inspect,
23
+ cb_object.send(:name_arguments_string),
24
+ cb_object.send(:rest_param_string),
25
+ cb_object.send(:kw_arguments_string),
26
+ cb_object.send(:keyrest_string),
27
+ cb_object.send(:procedure_string)].compact.join(', ')
28
+ target.instance_eval("send(\#{arguments})")
29
+ EOF
30
+ _wrapper = eval(proc_string)
31
+ _wrapper.instance_exec(raw_proc, &OVERLOAD_EQUALITY_OPERATOR_PROC)
32
+ _wrapper
33
+ end
34
+
35
+ private
36
+
37
+ # A that is instanced_exec'd on a new proc object within {#wrapper}
38
+ #
39
+ # Enables comparison of two wrapper procs to determine if they wrap the same
40
+ # Method.
41
+ OVERLOAD_EQUALITY_OPERATOR_PROC = Proc.new do |method_name|
42
+ def method_name
43
+ method_name
44
+ end
45
+
46
+ # Equality operator overload.
47
+ # If other is a {Symbol}, matches this object against {#method_name} defined above.
48
+ # If other is a {Proc}:
49
+ # * If it responds to {#method_name}, matches the method names of the two objects.
50
+ # * Otherwise false
51
+ #
52
+ # @param [Symbol] other A method name to compare against.
53
+ # @param [Proc] other A proc to compare against.
54
+ # @return [Boolean] Whether the two should be considered equivalent
55
+ def ==(other)
56
+ case other
57
+ when ::Proc
58
+ if other.respond_to?(:raw_proc)
59
+ self.method_name == other.method_name
60
+ else
61
+ false
62
+ end
63
+ when ::Symbol
64
+ self.method_name == other
65
+ else
66
+ false
67
+ end
68
+ end
69
+ end
70
+
71
+ def name_arguments_string
72
+ if name_params.any?
73
+ name_params.map{|name|
74
+ "name_proc.call(:#{name})"
75
+ }.join(', ')
76
+ end
77
+ end
78
+
79
+ def procedure_string
80
+ '&callbacks' if around_callback?
81
+ end
82
+
83
+ # @return [UnboundMethod] Method representation from class {#calling_class}, named by {#raw_proc}
84
+ def callback_method
85
+ @meth ||= calling_class.instance_method(raw_proc)
86
+ end
87
+
88
+ # Parameter definition for the object. See {UnboundMethod#parameters}
89
+ #
90
+ # @return [Array] Parameters
91
+ def parameters
92
+ callback_method.parameters
93
+ end
94
+
95
+ # @return [Fixnum] Arity of the callback method
96
+ def arity
97
+ callback_method.arity
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,48 @@
1
+
2
+ module Workflow
3
+ module Callbacks
4
+ module TransitionCallbacks
5
+ # A {Workflow::Callbacks::TransitionCallback} that wraps a callback proc.
6
+ class ProcWrapper < ::Workflow::Callbacks::TransitionCallback
7
+ # Builds a proc object that will correctly call the {#raw_proc}
8
+ # by inspecting its parameters and pulling arguments from the {Workflow::TransitionContext}
9
+ # object for the transition.
10
+ def wrapper
11
+ arguments = [
12
+ name_arguments_string,
13
+ rest_param_string,
14
+ kw_arguments_string,
15
+ keyrest_string,
16
+ procedure_string].compact.join(', ')
17
+
18
+ raw_proc = self.raw_proc
19
+ str = build_proc("target.instance_exec(#{arguments})")
20
+ eval(str)
21
+ end
22
+
23
+ private
24
+
25
+ def name_arguments_string
26
+ params = name_params
27
+ names = []
28
+ names << 'target' if params.shift
29
+ (names << 'callbacks') && params.shift if around_callback?
30
+ names += params.map{|name| "name_proc.call(:#{name})"}
31
+ return names.join(', ') if names.any?
32
+ end
33
+
34
+ def procedure_string
35
+ '&raw_proc'
36
+ end
37
+
38
+ def parameters
39
+ raw_proc.parameters
40
+ end
41
+
42
+ def arity
43
+ raw_proc.arity
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,8 +1,18 @@
1
1
  module Workflow
2
2
  class Event
3
+ # @!attribute [r] name
4
+ # @return [Symbol] The name of the event.
5
+ # @!attribute [r] transitions
6
+ # @return [Array] Array of {Workflow::Event::Transition}s defined for this event.
7
+ # @!attribute [r] meta
8
+ # @return [Hash] Extra information defined for this event.
3
9
  attr_reader :name, :transitions, :meta
4
10
 
5
- def initialize(name, meta)
11
+ # @api private
12
+ # See {Workflow::State#on} for creating objects of this class.
13
+ # @param [Symbol] name The name of the event to create.
14
+ # @param [Hash] meta: Optional Metadata for this object.
15
+ def initialize(name, meta: {})
6
16
  @name = name.to_sym
7
17
  @transitions = []
8
18
  @meta = meta || {}
@@ -12,27 +22,51 @@ module Workflow
12
22
  "<Event name=#{name.inspect} transitions(#{transitions.length})=#{transitions.inspect}>"
13
23
  end
14
24
 
25
+ # Returns the {Workflow::State} that the target object should enter.
26
+ # This will be the first one in the list of transitions, whose conditions apply
27
+ # to the target object in its present state.
28
+ # @param [Object] target An object of the class that this event was defined on.
29
+ # @return [Workflow::State] The first applicable destination state, or nil if none.
15
30
  def evaluate(target)
16
- transition = transitions.find{|t| t.apply? target}
17
- if transition
18
- return transition.target_state
19
- else
20
- nil
21
- end
31
+ transitions.find{|transition|
32
+ transition.matches? target
33
+ }&.target_state
22
34
  end
23
35
 
36
+ # Add a {Workflow::Transition} to the possible {#transitions} for this event.
37
+ #
38
+ # @param [Symbol] target_state the name of the state target state if this transition matches.
39
+ # @option conditions_def [Symbol] :if Name of instance method to evaluate. e.g. `:valid?`
40
+ # @option conditions_def [Array] :if Mixed array of Symbol, String or Proc conditions. All must match for the transition to apply.
41
+ # @option conditions_def [String] :if A string to evaluate on the target. e.g. `"self.foo == :bar"`
42
+ # @option conditions_def [Proc] :if A proc which will be evaluated on the object e.g. `->{self.foo == :bar}`
43
+ # @option conditions_def [Symbol] :unless Same as `:if` except all conditions must **not** match.
44
+ # @yield [] Optional block which, if provided, becomes an `:if` condition for the transition.
45
+ # @return [nil]
24
46
  def to(target_state, **conditions_def, &block)
25
47
  conditions = Conditions.new &&conditions_def, block
26
48
  self.transitions << Transition.new(target_state, conditions_def, &block)
27
49
  end
28
50
 
29
51
  private
52
+
53
+ # @api private
54
+ # Represents a possible transition via the event on which it is defined.
30
55
  class Transition
31
- attr_accessor :target_state, :conditions
32
- def apply?(target)
56
+ # @!attribute [r] target_state
57
+ # @return [Workflow::State] The target state for this transition.
58
+ attr_accessor :target_state
59
+
60
+ # Whether or not the conditions match for the target object.
61
+ #
62
+ # @param [Object] target an object of the class for which this event/transition was defined.
63
+ # @return [Boolean] True if all conditions apply.
64
+ def matches?(target)
33
65
  conditions.apply?(target)
34
66
  end
35
- # delegate :apply?, to: :conditions
67
+
68
+ # @param [Symbol] target_state the name of the state target state if this transition matches.
69
+ # @param [Hash] conditions_def See {Event#to}
36
70
  def initialize(target_state, conditions_def, &block)
37
71
  @target_state = target_state
38
72
  @conditions = Conditions.new conditions_def, &block
@@ -41,9 +75,19 @@ module Workflow
41
75
  def inspect
42
76
  "<to=#{target_state.inspect} conditions=#{conditions.inspect}"
43
77
  end
78
+
79
+ private
80
+ # @!attribute [r] conditions
81
+ # @return [Workflow::Event::Conditions] Conditions for this transition.
82
+ attr_reader :conditions
44
83
  end
45
84
 
46
- class Conditions #:nodoc:#
85
+ # @api private
86
+ # Maintains a list of callback procs which are evaluted to determine if the
87
+ # transition on which they were defined is valid for an object in its current state.
88
+ # Borrowed from ActiveSupport::Callbacks
89
+ # See [original source here](https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L419)
90
+ class Conditions
47
91
  def initialize(**options, &block)
48
92
  @if = Array(options[:if])
49
93
  @unless = Array(options[:unless])
@@ -61,7 +105,6 @@ module Workflow
61
105
 
62
106
  private
63
107
 
64
- # From https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L419
65
108
  def conditions_lambdas
66
109
  @if.map { |c| Callbacks::Callback.new c } +
67
110
  @unless.map { |c| Callbacks::Callback.new c, true }