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.
- 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
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Workflow
|
3
|
+
module Definition
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Returns a state object representing the current workflow state.
|
7
|
+
#
|
8
|
+
# @return [State] Current workflow state
|
9
|
+
def current_state
|
10
|
+
loaded_state = load_workflow_state
|
11
|
+
res = workflow_spec.states.find { |t| t.name == loaded_state.to_sym } if loaded_state
|
12
|
+
res || workflow_spec.initial_state
|
13
|
+
end
|
14
|
+
|
15
|
+
# The specification for this object.
|
16
|
+
# Could be set on a singleton for the object, on the object's class,
|
17
|
+
# Or else on a superclass of the object.
|
18
|
+
# @return [Specification] The Specification that applies to this object.
|
19
|
+
def workflow_spec
|
20
|
+
# check the singleton class first
|
21
|
+
class << self
|
22
|
+
return workflow_spec if workflow_spec
|
23
|
+
end
|
24
|
+
|
25
|
+
c = self.class
|
26
|
+
# using a simple loop instead of class_inheritable_accessor to avoid
|
27
|
+
# dependency on Rails' ActiveSupport
|
28
|
+
c = c.superclass until c.workflow_spec || !(c.include? Workflow)
|
29
|
+
c.workflow_spec
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
attr_reader :workflow_spec
|
34
|
+
|
35
|
+
##
|
36
|
+
# Define workflow for the class.
|
37
|
+
#
|
38
|
+
# @yield [] Specification of workflow. Example below and in README.markdown
|
39
|
+
# @return [nil]
|
40
|
+
#
|
41
|
+
# Workflow definition takes place inside the yielded block.
|
42
|
+
# @see Specification::state
|
43
|
+
# @see Specification::event
|
44
|
+
#
|
45
|
+
# ~~~ruby
|
46
|
+
#
|
47
|
+
# class Article
|
48
|
+
# include Workflow
|
49
|
+
# workflow do
|
50
|
+
# state :new do
|
51
|
+
# event :submit, :transitions_to => :awaiting_review
|
52
|
+
# end
|
53
|
+
# state :awaiting_review do
|
54
|
+
# event :review, :transitions_to => :being_reviewed
|
55
|
+
# end
|
56
|
+
# state :being_reviewed do
|
57
|
+
# event :accept, :transitions_to => :accepted
|
58
|
+
# event :reject, :transitions_to => :rejected
|
59
|
+
# end
|
60
|
+
# state :accepted
|
61
|
+
# state :rejected
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# ~~~
|
66
|
+
#
|
67
|
+
def workflow(&specification)
|
68
|
+
@workflow_spec = Specification.new({}, &specification)
|
69
|
+
HelperMethodConfigurator.new(@workflow_spec, self).configure!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/workflow/errors.rb
CHANGED
@@ -1,29 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Workflow
|
2
3
|
module Errors
|
3
4
|
class TransitionHaltedError < StandardError
|
4
|
-
|
5
5
|
attr_reader :halted_because
|
6
6
|
|
7
7
|
def initialize(msg = nil)
|
8
8
|
@halted_because = msg
|
9
9
|
super msg
|
10
10
|
end
|
11
|
-
|
12
11
|
end
|
13
12
|
|
14
13
|
class NoMatchingTransitionError < StandardError
|
15
14
|
end
|
16
15
|
|
17
|
-
class
|
16
|
+
class WorkflowDefinitionError < StandardError
|
18
17
|
end
|
19
18
|
|
20
|
-
class
|
19
|
+
class NoTransitionsDefinedError < WorkflowDefinitionError
|
20
|
+
def initialize(state, event)
|
21
|
+
super("No transitions defined for event [#{event.name}] on state [#{state.name}]")
|
22
|
+
end
|
21
23
|
end
|
22
24
|
|
23
|
-
class
|
25
|
+
class DualEventDefinitionError < WorkflowDefinitionError
|
26
|
+
def initialize
|
27
|
+
super('Event target can only be received in the method call or the block, not both.')
|
28
|
+
end
|
24
29
|
end
|
25
30
|
|
26
|
-
class
|
31
|
+
class EventNameCollisionError < WorkflowDefinitionError
|
32
|
+
def initialize(state, event_name)
|
33
|
+
super("Already defined an event [#{event_name}] for state[#{state.name}]")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class StateComparisonError < StandardError
|
38
|
+
def initialize(state)
|
39
|
+
super("Other State #{state} is a #{state.class}.
|
40
|
+
I can only be compared with a Workflow::State.".squish)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class NoSuchStateError < WorkflowDefinitionError
|
45
|
+
def initialize(event, transition)
|
46
|
+
super("Event #{event.name} transitions to
|
47
|
+
#{transition.target_state} but there is no such state.".squish)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class NoTransitionAllowed < StandardError
|
52
|
+
def initialize(state, event_name)
|
53
|
+
super("There is no event #{event_name} defined for the #{state.name} state")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class WorkflowError < StandardError
|
27
58
|
end
|
28
59
|
end
|
29
60
|
end
|
data/lib/workflow/event.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Workflow
|
2
3
|
class Event
|
3
4
|
# @!attribute [r] name
|
@@ -11,13 +12,17 @@ module Workflow
|
|
11
12
|
# @api private
|
12
13
|
# See {Workflow::State#on} for creating objects of this class.
|
13
14
|
# @param [Symbol] name The name of the event to create.
|
14
|
-
# @param [Hash] meta
|
15
|
+
# @param [Hash] meta Optional Metadata for this object.
|
15
16
|
def initialize(name, meta: {})
|
16
17
|
@name = name.to_sym
|
17
18
|
@transitions = []
|
18
19
|
@meta = meta || {}
|
19
20
|
end
|
20
21
|
|
22
|
+
def valid?
|
23
|
+
transitions.any?
|
24
|
+
end
|
25
|
+
|
21
26
|
def inspect
|
22
27
|
"<Event name=#{name.inspect} transitions(#{transitions.length})=#{transitions.inspect}>"
|
23
28
|
end
|
@@ -28,28 +33,37 @@ module Workflow
|
|
28
33
|
# @param [Object] target An object of the class that this event was defined on.
|
29
34
|
# @return [Workflow::State] The first applicable destination state, or nil if none.
|
30
35
|
def evaluate(target)
|
31
|
-
transitions.find
|
36
|
+
transitions.find do |transition|
|
32
37
|
transition.matches? target
|
33
|
-
|
38
|
+
end&.target_state
|
39
|
+
end
|
40
|
+
|
41
|
+
def evaluate!(target)
|
42
|
+
state_name = evaluate(target)
|
43
|
+
unless state_name
|
44
|
+
raise NoMatchingTransitionError, "No matching transition found on #{name}
|
45
|
+
for target #{target}. Consider adding a catchall transition.".squish
|
46
|
+
end
|
47
|
+
state_name
|
34
48
|
end
|
35
49
|
|
36
50
|
# Add a {Workflow::Transition} to the possible {#transitions} for this event.
|
37
51
|
#
|
38
52
|
# @param [Symbol] target_state the name of the state target state if this transition matches.
|
39
53
|
# @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.
|
41
|
-
#
|
42
|
-
# @option conditions_def [
|
43
|
-
#
|
54
|
+
# @option conditions_def [Array] :if Mixed array of Symbol, String or Proc conditions.
|
55
|
+
# All must match for the transition to apply.
|
56
|
+
# @option conditions_def [String] :if A string to evaluate on the target.
|
57
|
+
# e.g. `"self.foo == :bar"`
|
58
|
+
# @option conditions_def [Proc] :if A proc which will be evaluated on the object
|
59
|
+
# e.g. `->{self.foo == :bar}`
|
60
|
+
# @option conditions_def [Symbol] :unless Same Like `:if` but all conditions must **not** match
|
44
61
|
# @yield [] Optional block which, if provided, becomes an `:if` condition for the transition.
|
45
62
|
# @return [nil]
|
46
63
|
def to(target_state, **conditions_def, &block)
|
47
|
-
|
48
|
-
self.transitions << Transition.new(target_state, conditions_def, &block)
|
64
|
+
transitions << Transition.new(target_state, conditions_def, &block)
|
49
65
|
end
|
50
66
|
|
51
|
-
private
|
52
|
-
|
53
67
|
# @api private
|
54
68
|
# Represents a possible transition via the event on which it is defined.
|
55
69
|
class Transition
|
@@ -77,6 +91,7 @@ module Workflow
|
|
77
91
|
end
|
78
92
|
|
79
93
|
private
|
94
|
+
|
80
95
|
# @!attribute [r] conditions
|
81
96
|
# @return [Workflow::Event::Conditions] Conditions for this transition.
|
82
97
|
attr_reader :conditions
|
@@ -91,7 +106,7 @@ module Workflow
|
|
91
106
|
def initialize(**options, &block)
|
92
107
|
@if = Array(options[:if])
|
93
108
|
@unless = Array(options[:unless])
|
94
|
-
@if
|
109
|
+
@if << block if block_given?
|
95
110
|
@conditions_lambdas = conditions_lambdas
|
96
111
|
end
|
97
112
|
|
@@ -100,14 +115,14 @@ module Workflow
|
|
100
115
|
end
|
101
116
|
|
102
117
|
def apply?(target)
|
103
|
-
@conditions_lambdas.all?{|l| l.call(target)}
|
118
|
+
@conditions_lambdas.all? { |l| l.call(target) }
|
104
119
|
end
|
105
120
|
|
106
121
|
private
|
107
122
|
|
108
123
|
def conditions_lambdas
|
109
|
-
@if.map { |c| Callbacks::Callback.
|
110
|
-
@unless.map { |c| Callbacks::Callback.
|
124
|
+
@if.map { |c| Callbacks::Callback.build c } +
|
125
|
+
@unless.map { |c| Callbacks::Callback.inverted c }
|
111
126
|
end
|
112
127
|
end
|
113
128
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Workflow
|
3
|
+
class HelperMethodConfigurator
|
4
|
+
attr_reader :workflow_spec, :workflow_class
|
5
|
+
|
6
|
+
def initialize(workflow_spec, workflow_class)
|
7
|
+
@workflow_spec = workflow_spec
|
8
|
+
@workflow_class = workflow_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure!
|
12
|
+
undefine_methods_defined_by_workflow_spec if inherited_workflow_spec?
|
13
|
+
define_revert_events if workflow_spec.define_revert_events?
|
14
|
+
create_instance_methods
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_instance_methods
|
18
|
+
workflow_spec.states.each do |state|
|
19
|
+
state_name = state.name
|
20
|
+
workflow_class.module_eval do
|
21
|
+
define_method "#{state_name}?" do
|
22
|
+
state_name == current_state.name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
state.events.each do |event|
|
27
|
+
define_method_for_event(event) unless event_method?(event)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def define_revert_events
|
35
|
+
workflow_spec.states.each do |state|
|
36
|
+
reversible_events(state).each do |event|
|
37
|
+
revert_event_name = "revert_#{event.name}".to_sym
|
38
|
+
from_state_for_revert = event.transitions.first.target_state
|
39
|
+
from_state_for_revert.on revert_event_name, to: state
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def reversible_events(state)
|
45
|
+
state.events.select do |e|
|
46
|
+
e.name !~ /^revert_/ && e.transitions.length == 1
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def event_method?(event)
|
51
|
+
workflow_class.instance_methods.include?(event_method_name(event))
|
52
|
+
end
|
53
|
+
|
54
|
+
def event_method_name(event)
|
55
|
+
"#{event.name}!".to_sym
|
56
|
+
end
|
57
|
+
|
58
|
+
def define_method_for_event(event)
|
59
|
+
workflow_class.module_eval do
|
60
|
+
define_method "#{event.name}!".to_sym do |*args|
|
61
|
+
transition!(event.name, *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
define_method "can_#{event.name}?" do
|
65
|
+
current_state.find_event(event.name)&.evaluate(self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def inherited_workflow_spec?
|
71
|
+
workflow_class.superclass.respond_to?(:workflow_spec, true) &&
|
72
|
+
workflow_class.superclass.workflow_spec
|
73
|
+
end
|
74
|
+
|
75
|
+
def undefine_methods_defined_by_workflow_spec
|
76
|
+
superclass_workflow_spec.states.each do |state|
|
77
|
+
workflow_class.class_exec(state.name, &undef_state_method_proc)
|
78
|
+
|
79
|
+
state.events.each do |event|
|
80
|
+
workflow_class.class_exec(event.name, &undef_event_method_procs)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def superclass_workflow_spec
|
86
|
+
workflow_class.superclass.workflow_spec
|
87
|
+
end
|
88
|
+
|
89
|
+
def undef_state_method_proc
|
90
|
+
-> (state) { undef_method "#{state}?" }
|
91
|
+
end
|
92
|
+
|
93
|
+
def undef_event_method_procs
|
94
|
+
lambda do |event_name|
|
95
|
+
undef_method "#{event_name}!"
|
96
|
+
undef_method "can_#{event_name}?"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'workflow/state'
|
2
3
|
require 'workflow/event'
|
3
4
|
require 'workflow/errors'
|
@@ -24,17 +25,6 @@ module Workflow
|
|
24
25
|
attr_reader :named_arguments
|
25
26
|
|
26
27
|
define_callbacks :spec_definition
|
27
|
-
set_callback(:spec_definition, :after, if: :define_revert_events?) do |spec|
|
28
|
-
spec.states.each do |state|
|
29
|
-
state.events.reject{ |e|
|
30
|
-
e.name.to_s =~ /^revert_/
|
31
|
-
}.select{|e| e.transitions.length == 1}.each do |event|
|
32
|
-
revert_event_name = "revert_#{event.name}".to_sym
|
33
|
-
from_state_for_revert = event.transitions.first.target_state
|
34
|
-
from_state_for_revert.on revert_event_name, to: state
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
28
|
|
39
29
|
set_callback(:spec_definition, :after) do |spec|
|
40
30
|
spec.states.each do |state|
|
@@ -42,7 +32,7 @@ module Workflow
|
|
42
32
|
event.transitions.each do |transition|
|
43
33
|
target_state = spec.find_state(transition.target_state)
|
44
34
|
if target_state.nil?
|
45
|
-
raise
|
35
|
+
raise Errors::NoSuchStateError.new(event, transition)
|
46
36
|
end
|
47
37
|
transition.target_state = target_state
|
48
38
|
end
|
@@ -50,12 +40,14 @@ module Workflow
|
|
50
40
|
end
|
51
41
|
end
|
52
42
|
|
43
|
+
set_callback(:spec_definition, :after, :define_tag_methods)
|
44
|
+
|
53
45
|
# Find the state with the given name.
|
54
46
|
#
|
55
47
|
# @param [Symbol] name Name of state to find.
|
56
48
|
# @return [Workflow::State] The state with the given name.
|
57
49
|
def find_state(name)
|
58
|
-
states.find{|t| t.name == name.to_sym}
|
50
|
+
states.find { |t| t.name == name.to_sym }
|
59
51
|
end
|
60
52
|
|
61
53
|
# @api private
|
@@ -75,18 +67,17 @@ module Workflow
|
|
75
67
|
#
|
76
68
|
# @param [Symbol] name name of state
|
77
69
|
# @param [Hash] meta Metadata to be stored with the state within the {Specification} object
|
70
|
+
# @param [Array] tags Tags to apply to the {Workflow::State} object
|
78
71
|
# @yield [] block defining events for this state.
|
79
72
|
# @return [nil]
|
80
|
-
def state(name, meta: {}, &events)
|
73
|
+
def state(name, tags: [], meta: {}, &events)
|
81
74
|
name = name.to_sym
|
82
|
-
new_state = Workflow::State.new(name, @states.length, meta: meta)
|
75
|
+
new_state = Workflow::State.new(name, @states.length, tags: tags, meta: meta)
|
83
76
|
@initial_state ||= new_state
|
84
77
|
@states << new_state
|
85
78
|
new_state.instance_eval(&events) if block_given?
|
86
79
|
end
|
87
80
|
|
88
|
-
|
89
|
-
|
90
81
|
# Specify attributes to make available on the {TransitionContext} object
|
91
82
|
# during transitions taking place in this specification.
|
92
83
|
# The attributes' values will be taken in order from the arguments passed to
|
@@ -98,13 +89,12 @@ module Workflow
|
|
98
89
|
@named_arguments = names
|
99
90
|
end
|
100
91
|
|
101
|
-
|
102
92
|
# Also create additional event transitions that will move each configured transition
|
103
93
|
# in the reverse direction.
|
104
94
|
#
|
105
95
|
# @return [nil]
|
106
96
|
#
|
107
|
-
|
97
|
+
# ```ruby
|
108
98
|
# class Article
|
109
99
|
# include Workflow
|
110
100
|
# workflow do
|
@@ -121,17 +111,50 @@ module Workflow
|
|
121
111
|
# a.current_state.name # => :bax
|
122
112
|
# a.transition! :revert_bar
|
123
113
|
# a.current_state.name # => :foo
|
124
|
-
|
114
|
+
# ```
|
125
115
|
def define_revert_events!
|
126
116
|
@define_revert_events = true
|
127
117
|
end
|
128
118
|
|
119
|
+
def unique_event_names
|
120
|
+
states.collect(&:events).flatten.collect(&:name).flatten.uniq
|
121
|
+
end
|
122
|
+
|
123
|
+
def define_revert_events?
|
124
|
+
@define_revert_events
|
125
|
+
end
|
126
|
+
|
129
127
|
private
|
130
128
|
|
129
|
+
module TagHelpers
|
130
|
+
def initial?
|
131
|
+
sequence.zero?
|
132
|
+
end
|
131
133
|
|
132
|
-
|
133
|
-
|
134
|
+
def terminal?
|
135
|
+
events.empty?
|
136
|
+
end
|
134
137
|
end
|
135
138
|
|
139
|
+
def define_tag_methods
|
140
|
+
tags = states.map(&:tags).flatten.uniq
|
141
|
+
tag_method_module = build_tag_method_module(tags)
|
142
|
+
states.each do |state|
|
143
|
+
state.send :extend, tag_method_module
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_tag_method_module(tags)
|
148
|
+
tag_method_module = Module.new
|
149
|
+
tag_method_module.send :include, TagHelpers
|
150
|
+
tag_method_module.class_eval do
|
151
|
+
tags.each do |tag|
|
152
|
+
define_method "#{tag}?" do
|
153
|
+
self.tags.include?(tag)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
tag_method_module
|
158
|
+
end
|
136
159
|
end
|
137
160
|
end
|