rails-workflow 1.4.0
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 +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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 411f127c2c5c4be97813b1f68fe9e64224463dac
|
4
|
+
data.tar.gz: 2622936f556615b749b913f553b546b2782ae724
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e95a1eecf1dbc84a320e14027a9b3a4de4bb20ef8e8ca68ad41957ee91e65ebb8992957aab4814dd3d87c32522d19b93ffa6733979d2073337e778fc2e88457
|
7
|
+
data.tar.gz: b26bfe36a5d64de2b6d72735439fe334137fa0b73fb75e90a32b8305cd14b473a1d0378ac7ed9c3127e6e513e9182f18442d7d9ccda6a3ff2d6ac36a04758bbd
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
nbproject
|
2
|
+
html/
|
3
|
+
*.swp
|
4
|
+
*.gem
|
5
|
+
*.rbc
|
6
|
+
.bundle
|
7
|
+
.config
|
8
|
+
.yardoc
|
9
|
+
Gemfile*.lock
|
10
|
+
InstalledFiles
|
11
|
+
_yardoc
|
12
|
+
coverage
|
13
|
+
doc/
|
14
|
+
lib/bundler/man
|
15
|
+
pkg
|
16
|
+
rdoc
|
17
|
+
spec/reports
|
18
|
+
test/tmp
|
19
|
+
test/version_tmp
|
20
|
+
tmp
|
21
|
+
|
22
|
+
.byebug_history
|
data/.travis.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
before_install:
|
2
|
+
- sudo apt-get install -qq graphviz
|
3
|
+
before_script:
|
4
|
+
- gem list
|
5
|
+
- bundle show
|
6
|
+
|
7
|
+
rvm:
|
8
|
+
- 2.3.1
|
9
|
+
gemfile:
|
10
|
+
- gemfiles/Gemfile.rails-edge
|
11
|
+
|
12
|
+
matrix:
|
13
|
+
include:
|
14
|
+
- rvm: 1.9.3
|
15
|
+
gemfile: gemfiles/Gemfile.rails-3.x
|
16
|
+
|
17
|
+
- rvm: 2.0.0
|
18
|
+
gemfile: gemfiles/Gemfile.rails-3.x
|
19
|
+
|
20
|
+
- rvm: 2.0.0
|
21
|
+
gemfile: gemfiles/Gemfile.rails-4.0
|
22
|
+
|
23
|
+
- rvm: 2.3.1
|
24
|
+
gemfile: gemfiles/Gemfile.rails-4.0
|
25
|
+
|
26
|
+
- rvm: 2.3.1
|
27
|
+
gemfile: gemfiles/Gemfile.rails-5.0
|
data/.yardopts
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008-2009 Vodafone
|
2
|
+
Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,426 @@
|
|
1
|
+
What is workflow?
|
2
|
+
-----------------
|
3
|
+
|
4
|
+
This Gem is a fork of Vladimir Dobriakov's [Workflow Gem](http://github.com/geekq/workflow). Credit goes to him for the core code. Please read [the original README](http://github.com/geekq/workflow) for a full introduction,
|
5
|
+
as this README skims through much of that content and focuses on new / changed features.
|
6
|
+
|
7
|
+
## What's different in rails-workflow
|
8
|
+
|
9
|
+
The primary difference here is the use of [ActiveSupport::Callbacks](http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html)
|
10
|
+
to enable a more flexible application of callbacks.
|
11
|
+
You now have access to the same DSL you're used to from [ActionController Callbacks](http://guides.rubyonrails.org/action_controller_overview.html#filters),
|
12
|
+
including the ability to wrap state transitions in an `around_transition`, to place
|
13
|
+
conditional logic on application of callbacks, or to have callbacks run for only
|
14
|
+
a set of state-change events.
|
15
|
+
|
16
|
+
I've made `ActiveRecord` and `ActiveSupport` into runtime dependencies.
|
17
|
+
|
18
|
+
You can also take advantage of ActiveRecord's conditional validation syntax,
|
19
|
+
to apply validations only to specific state transitions.
|
20
|
+
|
21
|
+
|
22
|
+
Installation
|
23
|
+
------------
|
24
|
+
|
25
|
+
gem install rails-workflow
|
26
|
+
|
27
|
+
|
28
|
+
Ruby Version
|
29
|
+
--------
|
30
|
+
|
31
|
+
I've only tested with Ruby 2.3. ;) Time to upgrade.
|
32
|
+
|
33
|
+
|
34
|
+
# Basic workflow definition:
|
35
|
+
|
36
|
+
class Article
|
37
|
+
include Workflow
|
38
|
+
workflow do
|
39
|
+
state :new do
|
40
|
+
event :submit, :transitions_to => :awaiting_review
|
41
|
+
end
|
42
|
+
state :awaiting_review do
|
43
|
+
event :review, :transitions_to => :being_reviewed
|
44
|
+
end
|
45
|
+
state :being_reviewed do
|
46
|
+
event :accept, :transitions_to => :accepted
|
47
|
+
event :reject, :transitions_to => :rejected
|
48
|
+
end
|
49
|
+
state :accepted
|
50
|
+
state :rejected
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Access an object representing the current state of the entity,
|
55
|
+
including available events and transitions:
|
56
|
+
|
57
|
+
article.current_state
|
58
|
+
=> #<Workflow::State:0x7f1e3d6731f0 @events={
|
59
|
+
:submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
|
60
|
+
@transitions_to=:awaiting_review, @name=:submit, @meta={}>},
|
61
|
+
name:new, meta{}
|
62
|
+
|
63
|
+
On Ruby 1.9 and above, you can check whether a state comes before or
|
64
|
+
after another state (by the order they were defined):
|
65
|
+
|
66
|
+
article.current_state
|
67
|
+
=> being_reviewed
|
68
|
+
article.current_state < :accepted
|
69
|
+
=> true
|
70
|
+
article.current_state >= :accepted
|
71
|
+
=> false
|
72
|
+
article.current_state.between? :awaiting_review, :rejected
|
73
|
+
=> true
|
74
|
+
|
75
|
+
Now we can call the submit event, which transitions to the
|
76
|
+
<tt>:awaiting_review</tt> state:
|
77
|
+
|
78
|
+
article.submit!
|
79
|
+
article.awaiting_review? # => true
|
80
|
+
|
81
|
+
|
82
|
+
Callbacks
|
83
|
+
-------------------------
|
84
|
+
|
85
|
+
The DSL syntax here is very much similar to ActionController or ActiveRecord callbacks.
|
86
|
+
|
87
|
+
Callbacks with this strategy used the same as [ActionController Callbacks](http://guides.rubyonrails.org/action_controller_overview.html#filters).
|
88
|
+
|
89
|
+
You can configure any number of `before`, `around`, or `after` transition callbacks.
|
90
|
+
|
91
|
+
`before_transition` and `around_transition` are called in the order they are set,
|
92
|
+
and `after_transition` callbacks are called in reverse order.
|
93
|
+
|
94
|
+
## Around Transition
|
95
|
+
|
96
|
+
Allows you to run code surrounding the state transition.
|
97
|
+
|
98
|
+
around_transition :wrap_in_transaction
|
99
|
+
|
100
|
+
def wrap_in_transaction(&block)
|
101
|
+
Article.transaction(&block)
|
102
|
+
end
|
103
|
+
|
104
|
+
You can also define the callback using a block:
|
105
|
+
|
106
|
+
around_transition do |object, transition|
|
107
|
+
object.with_lock do
|
108
|
+
transition.call
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
### Replacement for workflow's `on_error` proc:
|
113
|
+
|
114
|
+
around_transition :catch_errors
|
115
|
+
|
116
|
+
def catch_errors
|
117
|
+
begin
|
118
|
+
yield
|
119
|
+
rescue SomeApplicationError => ex
|
120
|
+
logger.error 'Oh noes!'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
## before_transition
|
126
|
+
|
127
|
+
Allows you to run code prior to the state transition.
|
128
|
+
If you `halt` or `throw :abort` within a `before_transition`, the callback chain
|
129
|
+
will be halted, the transition will be canceled and the event action
|
130
|
+
will return false.
|
131
|
+
|
132
|
+
before_transition :check_title
|
133
|
+
|
134
|
+
def check_title
|
135
|
+
halt('Title was bad.') unless title == "Good Title"
|
136
|
+
end
|
137
|
+
|
138
|
+
Or again, in block expression:
|
139
|
+
|
140
|
+
before_transition do |article|
|
141
|
+
throw :abort unless article.title == "Good Title"
|
142
|
+
end
|
143
|
+
|
144
|
+
## After Transition
|
145
|
+
|
146
|
+
Runs code after the transition.
|
147
|
+
|
148
|
+
after_transition :check_title
|
149
|
+
|
150
|
+
|
151
|
+
## Prepend Transitions
|
152
|
+
|
153
|
+
To add a callback to the beginning of the sequence:
|
154
|
+
|
155
|
+
prepend_before_transition :some_before_transition
|
156
|
+
prepend_around_transition :some_around_transition
|
157
|
+
prepend_after_transition :some_after_transition
|
158
|
+
|
159
|
+
## Skip Transitions
|
160
|
+
|
161
|
+
skip_before_transition :some_before_transition
|
162
|
+
|
163
|
+
|
164
|
+
## Conditions
|
165
|
+
|
166
|
+
### if/unless
|
167
|
+
|
168
|
+
The callback will run `if` or `unless` the named method returns a truthy value.
|
169
|
+
|
170
|
+
before_transition :do_something, if: :valid?
|
171
|
+
|
172
|
+
### only/except
|
173
|
+
|
174
|
+
The callback will run `if` or `unless` the event being processed is in the list given
|
175
|
+
|
176
|
+
# Run this callback only on the `accept` and `publish` events.
|
177
|
+
before_transition :do_something, only: [:accept, :publish]
|
178
|
+
|
179
|
+
# Run this callback on events other than the `accept` and `publish` events.
|
180
|
+
before_transition :do_something_else, except: [:accept, :publish]
|
181
|
+
|
182
|
+
## Conditional Validations
|
183
|
+
|
184
|
+
If you are using `ActiveRecord`, you'll have access to a set of methods which
|
185
|
+
describe the current transition underway.
|
186
|
+
|
187
|
+
Inside the same Article class which was begun above, the following three
|
188
|
+
validations would all run when the `submit` event is used to transition
|
189
|
+
from `new` to `awaiting_review`.
|
190
|
+
|
191
|
+
validates :title, presence: true, if: :transitioning_to_awaiting_review?
|
192
|
+
validates :body, presence: true, if: :transitioning_from_new?
|
193
|
+
validates :author, presence: true, if: :transitioning_via_event_submit?
|
194
|
+
|
195
|
+
### Halting if validations fail
|
196
|
+
|
197
|
+
# This will create a transition callback which will stop the event
|
198
|
+
# and return false if validations fail.
|
199
|
+
halt_transition_unless_valid!
|
200
|
+
|
201
|
+
# This is the same as
|
202
|
+
|
203
|
+
### Checking A Transition
|
204
|
+
|
205
|
+
Call `can_transition?` to determine whether the validations would pass if a
|
206
|
+
given event was called:
|
207
|
+
|
208
|
+
if article.can_transition?(:submit)
|
209
|
+
# Do something interesting
|
210
|
+
end
|
211
|
+
|
212
|
+
# Transition Context
|
213
|
+
|
214
|
+
During transition you can refer to the `transition_context` object on your model,
|
215
|
+
for information about the current transition. See [Workflow::TransitionContext].
|
216
|
+
|
217
|
+
## Naming Event Arguments
|
218
|
+
|
219
|
+
If you will normally call each of your events with the same arguments, the following
|
220
|
+
will help:
|
221
|
+
|
222
|
+
class Article < ApplicationRecord
|
223
|
+
include Workflow
|
224
|
+
|
225
|
+
before_transition :check_reviewer
|
226
|
+
|
227
|
+
def check_reviewer
|
228
|
+
# Ability is a class from the cancan gem: https://github.com/CanCanCommunity/cancancan
|
229
|
+
halt('Access denied') unless Ability.new(transition_context.reviewer).can?(:review, self)
|
230
|
+
end
|
231
|
+
|
232
|
+
workflow do
|
233
|
+
event_args :reviewer, :reviewed_at
|
234
|
+
state :new do
|
235
|
+
event :review, transitions_to: :reviewed
|
236
|
+
end
|
237
|
+
state :reviewed
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
Transition event handler
|
243
|
+
------------------------
|
244
|
+
|
245
|
+
The best way is to use convention over configuration and to define a
|
246
|
+
method with the same name as the event. Then it is automatically invoked
|
247
|
+
when event is raised. For the Article workflow defined earlier it would
|
248
|
+
be:
|
249
|
+
|
250
|
+
class Article
|
251
|
+
def reject
|
252
|
+
puts 'sending email to the author explaining the reason...'
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
`article.review!; article.reject!` will cause state transition to
|
257
|
+
`being_reviewed` state, persist the new state (if integrated with
|
258
|
+
ActiveRecord), invoke this user defined `reject` method and finally
|
259
|
+
persist the `rejected` state.
|
260
|
+
|
261
|
+
Note: on successful transition from one state to another the workflow
|
262
|
+
gem immediately persists the new workflow state with `update_column()`,
|
263
|
+
bypassing any ActiveRecord callbacks including `updated_at` update.
|
264
|
+
This way it is possible to deal with the validation and to save the
|
265
|
+
pending changes to a record at some later point instead of the moment
|
266
|
+
when transition occurs.
|
267
|
+
|
268
|
+
You can also define event handler accepting/requiring additional
|
269
|
+
arguments:
|
270
|
+
|
271
|
+
class Article
|
272
|
+
def review(reviewer = '')
|
273
|
+
puts "[#{reviewer}] is now reviewing the article"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
article2 = Article.new
|
278
|
+
article2.submit!
|
279
|
+
article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
|
280
|
+
|
281
|
+
|
282
|
+
Integration with ActiveRecord
|
283
|
+
-----------------------------
|
284
|
+
|
285
|
+
Workflow library can handle the state persistence fully automatically. You
|
286
|
+
only need to define a string field on the table called `workflow_state`
|
287
|
+
and include the workflow mixin in your model class as usual:
|
288
|
+
|
289
|
+
class Order < ActiveRecord::Base
|
290
|
+
include Workflow
|
291
|
+
workflow do
|
292
|
+
# list states and transitions here
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
On a database record loading all the state check methods e.g.
|
297
|
+
`article.state`, `article.awaiting_review?` are immediately available.
|
298
|
+
For new records or if the `workflow_state` field is not set the state
|
299
|
+
defaults to the first state declared in the workflow specification. In
|
300
|
+
our example it is `:new`, so `Article.new.new?` returns true and
|
301
|
+
`Article.new.approved?` returns false.
|
302
|
+
|
303
|
+
At the end of a successful state transition like `article.approve!` the
|
304
|
+
new state is immediately saved in the database.
|
305
|
+
|
306
|
+
You can change this behaviour by overriding `persist_workflow_state`
|
307
|
+
method.
|
308
|
+
|
309
|
+
### Scopes
|
310
|
+
|
311
|
+
Workflow library also adds automatically generated scopes with names based on
|
312
|
+
states names:
|
313
|
+
|
314
|
+
class Order < ActiveRecord::Base
|
315
|
+
include Workflow
|
316
|
+
workflow do
|
317
|
+
state :approved
|
318
|
+
state :pending
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# returns all orders with `approved` state
|
323
|
+
Order.with_approved_state
|
324
|
+
|
325
|
+
# returns all orders with `pending` state
|
326
|
+
Order.with_pending_state
|
327
|
+
|
328
|
+
### Wrap State Transition in a locking transaction
|
329
|
+
|
330
|
+
Wrap your transition in a locking transaction to ensure that any exceptions
|
331
|
+
raised later in the transition sequence will roll back earlier changes made to
|
332
|
+
the record:
|
333
|
+
|
334
|
+
class Order < ActiveRecord::Base
|
335
|
+
include Workflow
|
336
|
+
workflow transactional: true do
|
337
|
+
state :approved
|
338
|
+
state :pending
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
Conditional event transitions
|
343
|
+
-----------------------------
|
344
|
+
|
345
|
+
Conditions can be a "method name symbol" with a corresponding instance method, a `proc` or `lambda` which are added to events, like so:
|
346
|
+
|
347
|
+
state :off
|
348
|
+
event :turn_on, :transition_to => :on,
|
349
|
+
:if => :sufficient_battery_level?
|
350
|
+
|
351
|
+
event :turn_on, :transition_to => :low_battery,
|
352
|
+
:if => proc { |device| device.battery_level > 0 }
|
353
|
+
end
|
354
|
+
|
355
|
+
# corresponding instance method
|
356
|
+
def sufficient_battery_level?
|
357
|
+
battery_level > 10
|
358
|
+
end
|
359
|
+
|
360
|
+
When calling a `device.can_<fire_event>?` check, or attempting a `device.<event>!`, each event is checked in turn:
|
361
|
+
|
362
|
+
* With no `:if` check, proceed as usual.
|
363
|
+
* If an `:if` check is present, proceed if it evaluates to true, or drop to the next event.
|
364
|
+
* If you've run out of events to check (eg. `battery_level == 0`), then the transition isn't possible.
|
365
|
+
|
366
|
+
|
367
|
+
|
368
|
+
Accessing your workflow specification
|
369
|
+
-------------------------------------
|
370
|
+
|
371
|
+
You can easily reflect on workflow specification programmatically - for
|
372
|
+
the whole class or for the current object. Examples:
|
373
|
+
|
374
|
+
article2.current_state.events # lists possible events from here
|
375
|
+
article2.current_state.events[:reject].transitions_to # => :rejected
|
376
|
+
|
377
|
+
Article.workflow_spec.states.keys
|
378
|
+
#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
|
379
|
+
|
380
|
+
Article.workflow_spec.state_names
|
381
|
+
#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
|
382
|
+
|
383
|
+
# list all events for all states
|
384
|
+
Article.workflow_spec.states.values.collect &:events
|
385
|
+
|
386
|
+
|
387
|
+
You can also store and later retrieve additional meta data for every
|
388
|
+
state and every event:
|
389
|
+
|
390
|
+
class MyProcess
|
391
|
+
include Workflow
|
392
|
+
workflow do
|
393
|
+
state :main, :meta => {:importance => 8}
|
394
|
+
state :supplemental, :meta => {:importance => 1}
|
395
|
+
end
|
396
|
+
end
|
397
|
+
puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
|
398
|
+
|
399
|
+
The workflow library itself uses this feature to tweak the graphical
|
400
|
+
representation of the workflow. See below.
|
401
|
+
|
402
|
+
|
403
|
+
Earlier versions
|
404
|
+
----------------
|
405
|
+
|
406
|
+
The `workflow` gem is the work of Vladimir Dobriakov, <http://www.mobile-web-consulting.de>, <http://blog.geekq.net/>.
|
407
|
+
|
408
|
+
This project is a fork of his work, and the bulk of the workflow specification code
|
409
|
+
and DSL are virtually unchanged.
|
410
|
+
|
411
|
+
|
412
|
+
About
|
413
|
+
-----
|
414
|
+
Author: Tyler Gannon [https://github.com/tylergannon]
|
415
|
+
|
416
|
+
Original Author: Vladimir Dobriakov, <http://www.mobile-web-consulting.de>, <http://blog.geekq.net/>
|
417
|
+
|
418
|
+
Copyright (c) 2010-2014 Vladimir Dobriakov, www.mobile-web-consulting.de
|
419
|
+
|
420
|
+
Copyright (c) 2008-2009 Vodafone
|
421
|
+
|
422
|
+
Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
|
423
|
+
|
424
|
+
Based on the work of Ryan Allen and Scott Barron
|
425
|
+
|
426
|
+
Licensed under MIT license, see the MIT-LICENSE file.
|