rails-workflow 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +27 -0
- data/.yardopts +2 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +426 -0
- data/Rakefile +30 -0
- data/asdf +18 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/Gemfile.rails-3.x +11 -0
- data/gemfiles/Gemfile.rails-4.0 +14 -0
- data/gemfiles/Gemfile.rails-5.0 +13 -0
- data/gemfiles/Gemfile.rails-edge +13 -0
- data/lib/workflow.rb +295 -0
- data/lib/workflow/adapters/active_record.rb +78 -0
- data/lib/workflow/adapters/active_record_validations.rb +110 -0
- data/lib/workflow/adapters/remodel.rb +15 -0
- data/lib/workflow/callbacks.rb +274 -0
- data/lib/workflow/configuration.rb +10 -0
- data/lib/workflow/draw.rb +79 -0
- data/lib/workflow/errors.rb +29 -0
- data/lib/workflow/event.rb +129 -0
- data/lib/workflow/specification.rb +137 -0
- data/lib/workflow/state.rb +88 -0
- data/lib/workflow/transition_context.rb +54 -0
- data/lib/workflow/version.rb +3 -0
- data/orders_workflow.png +0 -0
- data/rails-workflow.gemspec +51 -0
- metadata +258 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Workflow
|
4
|
+
module Adapter
|
5
|
+
module ActiveRecordValidations
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
###
|
9
|
+
#
|
10
|
+
# Captures instance method calls of the form `:transitioning_from_<state_name>`
|
11
|
+
# and `:transitioning_to_<state_name>`.
|
12
|
+
#
|
13
|
+
# For use with validators, e.g. `validates :foobar, presence: true, if: :transitioning_to_some_state?`
|
14
|
+
#
|
15
|
+
def method_missing(method, *args, &block)
|
16
|
+
if method.to_s =~ /^transitioning_(from|to|via_event)_([\w_-]+)\?$/
|
17
|
+
class_eval "
|
18
|
+
def #{method}
|
19
|
+
transitioning? direction: '#{$~[1]}', state: '#{$~[2]}'
|
20
|
+
end
|
21
|
+
"
|
22
|
+
send method
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?(context=nil)
|
29
|
+
if errors.any?
|
30
|
+
false
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def can_transition?(event_id)
|
37
|
+
event = current_state.find_event(event_id)
|
38
|
+
return false unless event
|
39
|
+
|
40
|
+
from = current_state.name
|
41
|
+
to = event.evaluate(self)
|
42
|
+
|
43
|
+
return false unless to
|
44
|
+
|
45
|
+
within_transition(from, to, event_id) do
|
46
|
+
valid?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
###
|
51
|
+
#
|
52
|
+
# Executes the given block within a context that is able to give
|
53
|
+
# correct answers to the questions, `:transitioning_from_<old_state>?`.
|
54
|
+
# `:transitioning_to_<new_state>`, `:transitioning_via_event_<event_name>?`
|
55
|
+
#
|
56
|
+
# For use with validators, e.g. `validates :foobar, presence: true, if: :transitioning_to_some_state?`
|
57
|
+
#
|
58
|
+
# = Example:
|
59
|
+
#
|
60
|
+
# before_transition do |from, to, name, *args|
|
61
|
+
# @halted = !within_transition from, to, name do
|
62
|
+
# valid?
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
def within_transition(from, to, event, &block)
|
67
|
+
begin
|
68
|
+
@transition_context = TransitionContext.new \
|
69
|
+
from: from,
|
70
|
+
to: to,
|
71
|
+
event: event,
|
72
|
+
attributes: {},
|
73
|
+
event_args: []
|
74
|
+
|
75
|
+
return block.call()
|
76
|
+
ensure
|
77
|
+
@transition_context = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods
|
82
|
+
def halt_transition_unless_valid!
|
83
|
+
before_transition unless: :valid? do |model|
|
84
|
+
throw :abort
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def wrap_transition_in_transaction!
|
89
|
+
around_transition do |model, transition|
|
90
|
+
model.with_lock do
|
91
|
+
transition.call
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def transitioning?(direction:, state:)
|
100
|
+
state = state.to_sym
|
101
|
+
return false unless transition_context
|
102
|
+
case direction
|
103
|
+
when 'from' then transition_context.from == state
|
104
|
+
when 'to' then transition_context.to == state
|
105
|
+
else transition_context.event == state
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Workflow
|
2
|
+
module Adapter
|
3
|
+
module Remodel
|
4
|
+
module InstanceMethods
|
5
|
+
def load_workflow_state
|
6
|
+
send(self.class.workflow_column)
|
7
|
+
end
|
8
|
+
|
9
|
+
def persist_workflow_state(new_value)
|
10
|
+
update(self.class.workflow_column => new_value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
module Workflow
|
2
|
+
module Callbacks
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include ActiveSupport::Callbacks
|
6
|
+
|
7
|
+
CALLBACK_MAP = {
|
8
|
+
transition: :event,
|
9
|
+
exit: :from,
|
10
|
+
enter: :to
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
# @!attribute [r] transition_context
|
14
|
+
# During state transition events, contains a {TransitionContext} representing the transition underway.
|
15
|
+
# @return [TransitionContext] representation of current state transition
|
16
|
+
#
|
17
|
+
included do
|
18
|
+
attr_reader :transition_context
|
19
|
+
CALLBACK_MAP.keys.each do |type|
|
20
|
+
define_callbacks type,
|
21
|
+
skip_after_callbacks_if_terminated: true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
##
|
27
|
+
# @!method before_transition
|
28
|
+
#
|
29
|
+
# :call-seq:
|
30
|
+
# before_transition(*instance_method_names, options={})
|
31
|
+
# before_transition(options={}, &block)
|
32
|
+
#
|
33
|
+
# Append a callback before transition.
|
34
|
+
# Instance methods used for `before` and `after` transitions
|
35
|
+
# receive no parameters. Instance methods for `around` transitions will be given a block,
|
36
|
+
# which must be yielded/called in order for the sequence to continue.
|
37
|
+
#
|
38
|
+
# Using a block notation, the first parameter will be an instance of the object
|
39
|
+
# under transition, while the second parameter (`around` transition only) will be
|
40
|
+
# the block which should be called for the sequence to continue.
|
41
|
+
#
|
42
|
+
# == Transition Metadata
|
43
|
+
#
|
44
|
+
# Within the callback you can access the `transition_context` instance variable,
|
45
|
+
# which will give you metadata and arguments passed to the transition.
|
46
|
+
# See Workflow::TransitionContext
|
47
|
+
#
|
48
|
+
# == Options
|
49
|
+
#
|
50
|
+
# === If / Unless
|
51
|
+
#
|
52
|
+
# The callback will run `if` or `unless` the named method returns a truthy value.
|
53
|
+
#
|
54
|
+
# # Assuming some_instance_method returns a boolean,
|
55
|
+
# before_transition :do_something, if: :some_instance_method
|
56
|
+
# before_transition :do_something_else, unless: :some_instance_method
|
57
|
+
#
|
58
|
+
# === Only / Except
|
59
|
+
#
|
60
|
+
# The callback will run `if` or `unless` the event being processed is in the list given
|
61
|
+
#
|
62
|
+
# # Run this callback only on the `accept` and `publish` events.
|
63
|
+
# before_transition :do_something, only: [:accept, :publish]
|
64
|
+
# # Run this callback on events other than the `accept` and `publish` events.
|
65
|
+
# before_transition :do_something_else, except: [:accept, :publish]
|
66
|
+
#
|
67
|
+
|
68
|
+
##
|
69
|
+
# @!method prepend_before_transition(*instance_method_names, options={})
|
70
|
+
#
|
71
|
+
# Something Interesting
|
72
|
+
#
|
73
|
+
# @overload prepend_before_transition(options={}, &block)
|
74
|
+
#
|
75
|
+
# Prepend a callback before transition, making it the first before transition called.
|
76
|
+
# Options are the same as for the standard #before_transition method.
|
77
|
+
|
78
|
+
##
|
79
|
+
# @!method skip_before_transition
|
80
|
+
#
|
81
|
+
# :call-seq: skip_before_transition(names)
|
82
|
+
#
|
83
|
+
# Skip a callback before transition.
|
84
|
+
# Options are the same as for the standard #before_transition method.
|
85
|
+
|
86
|
+
##
|
87
|
+
# @!method after_transition
|
88
|
+
#
|
89
|
+
# :call-seq:
|
90
|
+
# after_transition(*instance_method_names, options={})
|
91
|
+
# after_transition(options={}, &block)
|
92
|
+
#
|
93
|
+
# Append a callback after transition.
|
94
|
+
# Instance methods used for `before` and `after` transitions
|
95
|
+
# receive no parameters. Instance methods for `around` transitions will be given a block,
|
96
|
+
# which must be yielded/called in order for the sequence to continue.
|
97
|
+
#
|
98
|
+
# Using a block notation, the first parameter will be an instance of the object
|
99
|
+
# under transition, while the second parameter (`around` transition only) will be
|
100
|
+
# the block which should be called for the sequence to continue.
|
101
|
+
#
|
102
|
+
# == Transition Metadata
|
103
|
+
#
|
104
|
+
# Within the callback you can access the `transition_context` instance variable,
|
105
|
+
# which will give you metadata and arguments passed to the transition.
|
106
|
+
# See Workflow::TransitionContext
|
107
|
+
#
|
108
|
+
# == Options
|
109
|
+
#
|
110
|
+
# === If / Unless
|
111
|
+
#
|
112
|
+
# The callback will run `if` or `unless` the named method returns a truthy value.
|
113
|
+
#
|
114
|
+
# # Assuming some_instance_method returns a boolean,
|
115
|
+
# after_transition :do_something, if: :some_instance_method
|
116
|
+
# after_transition :do_something_else, unless: :some_instance_method
|
117
|
+
#
|
118
|
+
# === Only / Except
|
119
|
+
#
|
120
|
+
# The callback will run `if` or `unless` the event being processed is in the list given
|
121
|
+
#
|
122
|
+
# # Run this callback only on the `accept` and `publish` events.
|
123
|
+
# after_transition :do_something, only: [:accept, :publish]
|
124
|
+
# # Run this callback on events other than the `accept` and `publish` events.
|
125
|
+
# after_transition :do_something_else, except: [:accept, :publish]
|
126
|
+
#
|
127
|
+
|
128
|
+
##
|
129
|
+
# @!method prepend_after_transition(*instance_method_names, options={})
|
130
|
+
#
|
131
|
+
# Something Interesting
|
132
|
+
#
|
133
|
+
# @overload prepend_after_transition(options={}, &block)
|
134
|
+
#
|
135
|
+
# Prepend a callback after transition, making it the first after transition called.
|
136
|
+
# Options are the same as for the standard #after_transition method.
|
137
|
+
|
138
|
+
##
|
139
|
+
# @!method skip_after_transition
|
140
|
+
#
|
141
|
+
# :call-seq: skip_after_transition(names)
|
142
|
+
#
|
143
|
+
# Skip a callback after transition.
|
144
|
+
# Options are the same as for the standard #after_transition method.
|
145
|
+
|
146
|
+
##
|
147
|
+
# @!method around_transition
|
148
|
+
#
|
149
|
+
# :call-seq:
|
150
|
+
# around_transition(*instance_method_names, options={})
|
151
|
+
# around_transition(options={}, &block)
|
152
|
+
#
|
153
|
+
# Append a callback around transition.
|
154
|
+
# Instance methods used for `before` and `after` transitions
|
155
|
+
# receive no parameters. Instance methods for `around` transitions will be given a block,
|
156
|
+
# which must be yielded/called in order for the sequence to continue.
|
157
|
+
#
|
158
|
+
# Using a block notation, the first parameter will be an instance of the object
|
159
|
+
# under transition, while the second parameter (`around` transition only) will be
|
160
|
+
# the block which should be called for the sequence to continue.
|
161
|
+
#
|
162
|
+
# == Transition Metadata
|
163
|
+
#
|
164
|
+
# Within the callback you can access the `transition_context` instance variable,
|
165
|
+
# which will give you metadata and arguments passed to the transition.
|
166
|
+
# See Workflow::TransitionContext
|
167
|
+
#
|
168
|
+
# == Options
|
169
|
+
#
|
170
|
+
# === If / Unless
|
171
|
+
#
|
172
|
+
# The callback will run `if` or `unless` the named method returns a truthy value.
|
173
|
+
#
|
174
|
+
# # Assuming some_instance_method returns a boolean,
|
175
|
+
# around_transition :do_something, if: :some_instance_method
|
176
|
+
# around_transition :do_something_else, unless: :some_instance_method
|
177
|
+
#
|
178
|
+
# === Only / Except
|
179
|
+
#
|
180
|
+
# The callback will run `if` or `unless` the event being processed is in the list given
|
181
|
+
#
|
182
|
+
# # Run this callback only on the `accept` and `publish` events.
|
183
|
+
# around_transition :do_something, only: [:accept, :publish]
|
184
|
+
# # Run this callback on events other than the `accept` and `publish` events.
|
185
|
+
# around_transition :do_something_else, except: [:accept, :publish]
|
186
|
+
#
|
187
|
+
|
188
|
+
##
|
189
|
+
# @!method prepend_around_transition(*instance_method_names, options={})
|
190
|
+
#
|
191
|
+
# Something Interesting
|
192
|
+
#
|
193
|
+
# @overload prepend_around_transition(options={}, &block)
|
194
|
+
#
|
195
|
+
# Prepend a callback around transition, making it the first around transition called.
|
196
|
+
# Options are the same as for the standard #around_transition method.
|
197
|
+
|
198
|
+
##
|
199
|
+
# @!method skip_around_transition
|
200
|
+
#
|
201
|
+
# :call-seq: skip_around_transition(names)
|
202
|
+
#
|
203
|
+
# Skip a callback around transition.
|
204
|
+
# Options are the same as for the standard #around_transition method.
|
205
|
+
|
206
|
+
[:before, :after, :around].each do |callback|
|
207
|
+
CALLBACK_MAP.each do |type, context_attribute|
|
208
|
+
define_method "#{callback}_#{type}" do |*names, &blk|
|
209
|
+
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
210
|
+
set_callback(type, callback, name, options)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
define_method "prepend_#{callback}_#{type}" do |*names, &blk|
|
215
|
+
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
216
|
+
set_callback(type, callback, name, options.merge(prepend: true))
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Skip a before, after or around callback. See _insert_callbacks
|
221
|
+
# for details on the allowed parameters.
|
222
|
+
define_method "skip_#{callback}_#{type}" do |*names|
|
223
|
+
_insert_callbacks(names, context_attribute) do |name, options|
|
224
|
+
skip_callback(type, callback, name, options)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# *_action is the same as append_*_action
|
229
|
+
alias_method :"append_#{callback}_#{type}", :"#{callback}_#{type}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
def _insert_callbacks(callbacks, context_attribute, block = nil)
|
235
|
+
options = callbacks.extract_options!
|
236
|
+
_normalize_callback_options(options, context_attribute)
|
237
|
+
callbacks.push(block) if block
|
238
|
+
callbacks.each do |callback|
|
239
|
+
yield callback, options
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def _normalize_callback_options(options, context_attribute)
|
244
|
+
_normalize_callback_option(options, context_attribute, :only, :if)
|
245
|
+
_normalize_callback_option(options, context_attribute, :except, :unless)
|
246
|
+
end
|
247
|
+
|
248
|
+
def _normalize_callback_option(options, context_attribute, from, to) # :nodoc:
|
249
|
+
if from = options[from]
|
250
|
+
_from = Array(from).map(&:to_sym).to_set
|
251
|
+
from = proc { |record|
|
252
|
+
_from.include? record.transition_context.send(context_attribute).to_sym
|
253
|
+
}
|
254
|
+
options[to] = Array(options[to]).unshift(from)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
# TODO: Do something here.
|
261
|
+
def halted_callback_hook(filter)
|
262
|
+
end
|
263
|
+
|
264
|
+
def run_all_callbacks(&block)
|
265
|
+
catch(:abort) do
|
266
|
+
run_callbacks :transition do
|
267
|
+
throw(:abort) if false == run_callbacks(:exit) do
|
268
|
+
throw(:abort) if false == run_callbacks(:enter, &block)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
gem 'ruby-graphviz', '~> 1.0.0'
|
5
|
+
gem 'activesupport'
|
6
|
+
|
7
|
+
require 'graphviz'
|
8
|
+
require 'active_support/inflector'
|
9
|
+
rescue LoadError => e
|
10
|
+
$stderr.puts "Could not load the ruby-graphiz or active_support gems for rendering: #{e.message}"
|
11
|
+
end
|
12
|
+
|
13
|
+
module Workflow
|
14
|
+
module Draw
|
15
|
+
|
16
|
+
# Generates a `dot` graph of the workflow.
|
17
|
+
# Prerequisite: the `dot` binary. (Download from http://www.graphviz.org/)
|
18
|
+
# You can use this method in your own Rakefile like this:
|
19
|
+
#
|
20
|
+
# namespace :doc do
|
21
|
+
# desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
|
22
|
+
# task :workflow => :environment do
|
23
|
+
# require 'workflow/draw'
|
24
|
+
# Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# You can influence the placement of nodes by specifying
|
29
|
+
# additional meta information in your states and transition descriptions.
|
30
|
+
# You can assign higher `weight` value to the typical transitions
|
31
|
+
# in your workflow. All other states and transitions will be arranged
|
32
|
+
# around that main line. See also `weight` in the graphviz documentation.
|
33
|
+
# Example:
|
34
|
+
#
|
35
|
+
# state :new do
|
36
|
+
# event :approve, :transitions_to => :approved, :meta => {:weight => 8}
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# @param klass A class with the Workflow mixin, for which you wish the graphical workflow representation
|
41
|
+
# @param [String] target_dir Directory, where to save the dot and the pdf files
|
42
|
+
# @param [String] graph_options You can change graph orientation, size etc. See graphviz documentation
|
43
|
+
def self.workflow_diagram(klass, options={})
|
44
|
+
options = {
|
45
|
+
:name => "#{klass.name.tableize}_workflow".gsub('/', '_'),
|
46
|
+
:path => '.',
|
47
|
+
:orientation => "landscape",
|
48
|
+
:ratio => "fill",
|
49
|
+
:format => 'png',
|
50
|
+
:font => 'Helvetica'
|
51
|
+
}.merge options
|
52
|
+
|
53
|
+
graph = ::GraphViz.new('G', :rankdir => options[:orientation] == 'landscape' ? 'LR' : 'TB', :ratio => options[:ratio])
|
54
|
+
|
55
|
+
# Add nodes
|
56
|
+
klass.workflow_spec.states.each do |_, state|
|
57
|
+
node = state.draw(graph)
|
58
|
+
node.fontname = options[:font]
|
59
|
+
|
60
|
+
state.uniq_events.each do |event|
|
61
|
+
edge = event.draw(graph, state)
|
62
|
+
edge.fontname = options[:font]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Generate the graph
|
67
|
+
filename = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
|
68
|
+
|
69
|
+
graph.output options[:format] => "'#{filename}'"
|
70
|
+
|
71
|
+
puts "
|
72
|
+
Please run the following to open the generated file:
|
73
|
+
|
74
|
+
open '#{filename}'
|
75
|
+
"
|
76
|
+
graph
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|