rails-workflow 1.4.4.4 → 1.4.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 }