rails-workflow 1.4.1.2 → 1.4.1.3
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/README.markdown +388 -193
- data/lib/workflow.rb +4 -4
- data/lib/workflow/adapters/active_record.rb +1 -0
- data/lib/workflow/adapters/active_record_validations.rb +38 -9
- data/lib/workflow/callbacks.rb +53 -3
- data/lib/workflow/callbacks/callback.rb +85 -0
- data/lib/workflow/callbacks/transition_callback_wrapper.rb +100 -0
- data/lib/workflow/event.rb +3 -61
- data/lib/workflow/specification.rb +3 -3
- data/lib/workflow/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8270a626780c254a58897c2cef2e0b162b5fbcae
|
4
|
+
data.tar.gz: 1a9e4480f3306fc8b10136d2dce4c92d3570b529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70c376c21353280fbff28bac40ba2509581c681e2591788793d1031bf40923f21665c2e5285609e899e65a3b74acb4390d511cd14108beeb8b438ab861e82085
|
7
|
+
data.tar.gz: b79da68fd8e266ff0d1a55623a60db87c1499dfeb0ccfc0e551dd4ee77186b4ee57b5e5f8f26fdc6665db07c8caf0fac71419926b3b0fef9b1bcc42f37e5de1b
|
data/README.markdown
CHANGED
@@ -1,126 +1,223 @@
|
|
1
1
|
What is workflow?
|
2
2
|
-----------------
|
3
3
|
|
4
|
-
This Gem is a fork of Vladimir Dobriakov's [Workflow Gem](http://github.com/geekq/workflow). Credit goes to him for the
|
5
|
-
as this README skims through much of that content and focuses on new / changed features.
|
4
|
+
This Gem is a fork of Vladimir Dobriakov's [Workflow Gem](http://github.com/geekq/workflow). Credit goes to him for the inspiration, architecture and basic syntax.
|
6
5
|
|
7
6
|
## What's different in rails-workflow
|
8
7
|
|
9
|
-
|
8
|
+
* Use of [ActiveSupport::Callbacks](http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html)
|
10
9
|
to enable a more flexible application of callbacks.
|
11
|
-
|
12
|
-
|
13
|
-
conditional logic on application of callbacks, or to have callbacks run for only
|
14
|
-
a set of state-change events.
|
10
|
+
* Slightly terser syntax for event definition.
|
11
|
+
* Cleaner support for using conditional ActiveRecord validations to validate state transitions.
|
15
12
|
|
16
|
-
I've made `ActiveRecord` and `ActiveSupport` into runtime dependencies.
|
17
13
|
|
18
|
-
|
19
|
-
to apply validations only to specific state transitions.
|
14
|
+
## Installation
|
20
15
|
|
16
|
+
gem install rails-workflow
|
21
17
|
|
22
|
-
|
23
|
-
------------
|
18
|
+
## Configuration
|
24
19
|
|
25
|
-
|
20
|
+
No configuraion is required, but the following configurations can be placed inside an initializer:
|
26
21
|
|
22
|
+
```ruby
|
23
|
+
# config/initializers/workflow.rb
|
24
|
+
Workflow.configure do |config|
|
25
|
+
# Set false to avoid the extra call to the database, if you'll be saving the object after transition.
|
26
|
+
self.persist_workflow_state_immediately = true
|
27
|
+
# Set true to also change the `:updated_at` during state transition.
|
28
|
+
self.touch_on_update_column = false
|
29
|
+
end
|
30
|
+
|
31
|
+
```
|
27
32
|
|
28
33
|
Ruby Version
|
29
34
|
--------
|
30
35
|
|
31
|
-
I've only tested with Ruby 2.3. ;)
|
32
|
-
|
36
|
+
I've only tested with Ruby 2.3. ;)
|
33
37
|
|
34
38
|
# Basic workflow definition:
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
state :accepted
|
50
|
-
state :rejected
|
51
|
-
end
|
40
|
+
```ruby
|
41
|
+
class Article
|
42
|
+
include Workflow
|
43
|
+
workflow do
|
44
|
+
state :new do
|
45
|
+
on :submit, to: :awaiting_review
|
46
|
+
end
|
47
|
+
state :awaiting_review do
|
48
|
+
on :review, to: :being_reviewed
|
49
|
+
end
|
50
|
+
state :being_reviewed do
|
51
|
+
on :accept, to: :accepted
|
52
|
+
on :reject, to: :rejected
|
52
53
|
end
|
54
|
+
state :accepted
|
55
|
+
state :rejected
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
```
|
60
|
+
|
61
|
+
## Invoking State Transitions
|
62
|
+
|
63
|
+
You may call the method named for the event itself, or else the more generic `transition!` method
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
a = Article.new
|
67
|
+
a.current_state.name
|
68
|
+
# => :new
|
69
|
+
a.submit!
|
70
|
+
a.current_state.name
|
71
|
+
# => :awaiting_review
|
72
|
+
# ... etc
|
73
|
+
```
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
a = Article.new
|
77
|
+
a.transition! :submit
|
78
|
+
a.current_state.name
|
79
|
+
# => :awaiting_review
|
80
|
+
```
|
81
|
+
|
82
|
+
The transition will return a truthy value if it succeeds: either the return value
|
83
|
+
of the event-specific callback, if one is defined, or else the name of the new state
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
puts a.transition!(:submit)
|
87
|
+
# => :awaiting_review
|
88
|
+
```
|
89
|
+
|
90
|
+
If the transition does not finish and no exception is raised, the method returns `false`.
|
91
|
+
|
92
|
+
Generally this would be because of a validation failure, so checking the model for errors
|
93
|
+
would be the next course of action.
|
94
|
+
|
95
|
+
You can also pass arguments to the event, though nothing will happen with them except
|
96
|
+
as you've defined in your callbacks (described below)
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
a.submit!(author: 'Fanny Schmittenbauer', awesomeness: 29)
|
100
|
+
```
|
53
101
|
|
54
102
|
Access an object representing the current state of the entity,
|
55
103
|
including available events and transitions:
|
56
104
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
name:new, meta{}
|
105
|
+
```ruby
|
106
|
+
article.current_state
|
107
|
+
# => <State name=:new events(1)=[<Event name=:submit transitions(1)=[<to=<State name=:awaiting_review events(1)=[<Event name=:review transitions(1)=...
|
108
|
+
```
|
62
109
|
|
63
110
|
On Ruby 1.9 and above, you can check whether a state comes before or
|
64
111
|
after another state (by the order they were defined):
|
65
112
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
113
|
+
```ruby
|
114
|
+
article.current_state
|
115
|
+
# => being_reviewed
|
116
|
+
article.current_state < :accepted
|
117
|
+
# => true
|
118
|
+
article.current_state >= :accepted
|
119
|
+
# => false
|
120
|
+
article.current_state.between? :awaiting_review, :rejected
|
121
|
+
# => true
|
122
|
+
```
|
75
123
|
Now we can call the submit event, which transitions to the
|
76
124
|
<tt>:awaiting_review</tt> state:
|
77
125
|
|
78
126
|
article.submit!
|
79
127
|
article.awaiting_review? # => true
|
80
128
|
|
129
|
+
# Multiple Possible Targets For A Given Event
|
130
|
+
|
131
|
+
The first matching condition will determine the target state.
|
132
|
+
An error will be raised if none match, so a catchall at the end is a good idea.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class Article
|
136
|
+
include Workflow
|
137
|
+
workflow do
|
138
|
+
state :new do
|
139
|
+
on :submit do
|
140
|
+
to :awaiting_review, if: :today_is_wednesday?
|
141
|
+
to :being_reviewed, unless: "author.name == 'Foo Bar'"
|
142
|
+
to :accepted, if: -> {author.role == 'Admin'}
|
143
|
+
to :rejected, if: [:bad_hair_day?, :in_a_bad_mood?]
|
144
|
+
to :the_bad_place
|
145
|
+
end
|
146
|
+
end
|
147
|
+
state :awaiting_review do
|
148
|
+
on :review, to: :being_reviewed
|
149
|
+
end
|
150
|
+
state :being_reviewed do
|
151
|
+
on :accept, to: :accepted
|
152
|
+
on :reject, to: :rejected
|
153
|
+
end
|
154
|
+
state :accepted
|
155
|
+
state :rejected
|
156
|
+
state :the_bad_place
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
81
160
|
|
82
161
|
Callbacks
|
83
162
|
-------------------------
|
84
163
|
|
85
164
|
The DSL syntax here is very much similar to ActionController or ActiveRecord callbacks.
|
86
165
|
|
87
|
-
|
166
|
+
Three classes of callbacks:
|
88
167
|
|
89
|
-
|
168
|
+
* :transition callbacks representing named events.
|
169
|
+
* `before_transition only: :submit`
|
170
|
+
* `after_transition except: :submit`
|
171
|
+
* :exit callbacks that match on the state the transition leaves
|
172
|
+
* `before_exit only: :being_reviewed #will run on the :accept or the :reject event`
|
173
|
+
* :enter callbacks that match on the target state for the transition
|
174
|
+
* `before_enter only: :being_reviewed` #will run on the :review event
|
90
175
|
|
91
|
-
|
92
|
-
and `after_transition` callbacks are called in reverse order.
|
176
|
+
Callbacks run in this order:
|
93
177
|
|
94
|
-
|
178
|
+
* `before_transition`, `around_transition`
|
179
|
+
* `before_exit`, `around_exit`
|
180
|
+
* `before_enter`, `around_enter`
|
181
|
+
* **State Transition**
|
182
|
+
* `after_enter`
|
183
|
+
* `after_exit`
|
184
|
+
* `after_transition`
|
95
185
|
|
96
|
-
|
186
|
+
Within each group, the callbacks fire in the order they are set.
|
97
187
|
|
98
|
-
|
188
|
+
### Halting callbacks
|
189
|
+
Inside any `:before` callback, you can halt the callback chain:
|
99
190
|
|
100
|
-
|
101
|
-
|
102
|
-
|
191
|
+
```ruby
|
192
|
+
before_enter do
|
193
|
+
throw :abort
|
194
|
+
end
|
195
|
+
```
|
103
196
|
|
104
|
-
|
197
|
+
Note that this will halt the callback chain without an error,
|
198
|
+
so you won't get an exception in your `on_error` block, if you have one.
|
105
199
|
|
106
|
-
|
107
|
-
object.with_lock do
|
108
|
-
transition.call
|
109
|
-
end
|
110
|
-
end
|
200
|
+
## Around Transition
|
111
201
|
|
112
|
-
|
202
|
+
Allows you to run code surrounding the state transition.
|
113
203
|
|
114
|
-
|
204
|
+
```ruby
|
205
|
+
around_transition :wrap_in_transaction
|
115
206
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
logger.error 'Oh noes!'
|
121
|
-
end
|
122
|
-
end
|
207
|
+
def wrap_in_transaction(&block)
|
208
|
+
Article.transaction(&block)
|
209
|
+
end
|
210
|
+
```
|
123
211
|
|
212
|
+
You can also define the callback using a block:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
around_transition do |object, transition|
|
216
|
+
object.with_lock do
|
217
|
+
transition.call
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
124
221
|
|
125
222
|
## before_transition
|
126
223
|
|
@@ -129,36 +226,44 @@ If you `halt` or `throw :abort` within a `before_transition`, the callback chain
|
|
129
226
|
will be halted, the transition will be canceled and the event action
|
130
227
|
will return false.
|
131
228
|
|
132
|
-
|
229
|
+
```ruby
|
230
|
+
before_transition :check_title
|
133
231
|
|
134
232
|
def check_title
|
135
233
|
halt('Title was bad.') unless title == "Good Title"
|
136
234
|
end
|
235
|
+
```
|
137
236
|
|
138
237
|
Or again, in block expression:
|
139
238
|
|
239
|
+
```ruby
|
140
240
|
before_transition do |article|
|
141
241
|
throw :abort unless article.title == "Good Title"
|
142
242
|
end
|
143
|
-
|
243
|
+
```
|
144
244
|
## After Transition
|
145
245
|
|
146
246
|
Runs code after the transition.
|
147
247
|
|
248
|
+
```ruby
|
148
249
|
after_transition :check_title
|
149
|
-
|
250
|
+
```
|
150
251
|
|
151
252
|
## Prepend Transitions
|
152
253
|
|
153
254
|
To add a callback to the beginning of the sequence:
|
154
255
|
|
155
|
-
|
156
|
-
|
157
|
-
|
256
|
+
```ruby
|
257
|
+
prepend_before_transition :some_before_transition
|
258
|
+
prepend_around_transition :some_around_transition
|
259
|
+
prepend_after_transition :some_after_transition
|
260
|
+
```
|
158
261
|
|
159
262
|
## Skip Transitions
|
160
263
|
|
264
|
+
```ruby
|
161
265
|
skip_before_transition :some_before_transition
|
266
|
+
```
|
162
267
|
|
163
268
|
|
164
269
|
## Conditions
|
@@ -167,17 +272,114 @@ To add a callback to the beginning of the sequence:
|
|
167
272
|
|
168
273
|
The callback will run `if` or `unless` the named method returns a truthy value.
|
169
274
|
|
170
|
-
|
275
|
+
```ruby
|
276
|
+
before_transition :do_something, if: :valid?
|
277
|
+
|
278
|
+
# Array conditions apply if all aggregated conditions apply.
|
279
|
+
before_transition :do_something, if: [:valid?, :kosher?]
|
280
|
+
before_transition :do_something, if: [:valid?, "title == 'Good Title'"]
|
281
|
+
before_transition :do_something, unless: [:valid?, -> {title == 'Good Title'}]
|
282
|
+
```
|
171
283
|
|
172
284
|
### only/except
|
173
285
|
|
174
|
-
The callback
|
286
|
+
The three callback classes accept `:only` and `:except` parameters, and treat them slightly differnetly.
|
287
|
+
|
288
|
+
You can use `:only` and `:except` in conjunction with `:if` and `:unless`.
|
289
|
+
|
290
|
+
* **Transition Callbacks** match on the name of the event being executed.
|
291
|
+
* `before_transition only: :submit` will run when the `:submit` event is fired
|
292
|
+
* `before_transition except: [:submit, :reject]` will run on any event except the two named
|
293
|
+
* **Exit Callbacks** match on the name of the state being exited
|
294
|
+
* `before_exit only: :new` will run when an event causes the object to leave the `:new` state.
|
295
|
+
* **Enter Callbacks** match on the name of the state being entered
|
296
|
+
* `before_enter only: [:cancelled, :rejected]` will run when an event leaves the object `:cancelled` or `:rejected`.
|
297
|
+
|
298
|
+
## Parameterized Callbacks
|
299
|
+
|
300
|
+
If you're passing parameters through the `transition!` method, you can receive
|
301
|
+
them easily in your callbacks. For example:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class Article
|
305
|
+
include Workflow
|
306
|
+
workflow do
|
307
|
+
event_args :review_date
|
308
|
+
end
|
309
|
+
before_transition do |reviewer:|
|
310
|
+
logger.debug reviewer.name
|
311
|
+
end
|
312
|
+
after_transition do |author:, **arguments|
|
313
|
+
logger.debug arguments[:reviewer].name
|
314
|
+
logger.debug author.name
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
Article.last.transition! :submit, reviewer: current_user
|
319
|
+
```
|
320
|
+
|
321
|
+
If you don't like keyword arguments you can use standard arguments, but you
|
322
|
+
need to receive the model as the first argument to your block, and you have to
|
323
|
+
configure the `event_args` for the transition context, within your workflow definition.
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
before_transition, only: :submit do |article, review_date, reviewer:|
|
327
|
+
puts review_date
|
328
|
+
end
|
329
|
+
|
330
|
+
Article.last.transition! :submit, Date.today, reviewer: current_user
|
331
|
+
|
332
|
+
```
|
333
|
+
|
334
|
+
## Catching Errors
|
175
335
|
|
176
|
-
|
177
|
-
|
336
|
+
```ruby
|
337
|
+
class WorkflowModel
|
338
|
+
include Workflow
|
178
339
|
|
179
|
-
|
180
|
-
|
340
|
+
# Some possibilities:
|
341
|
+
on_error StandardError, rescue: "self.errors << 'oops!'"
|
342
|
+
on_error StandardError, rescue: :notify_error_service!
|
343
|
+
|
344
|
+
# Default error class is Exception
|
345
|
+
on_error unless: "logger.nil?" do |ex|
|
346
|
+
logger.warn ex.message
|
347
|
+
raise ApplicationError.new('Whoopsies!')
|
348
|
+
end
|
349
|
+
|
350
|
+
on_error ensure: ->{self.always_run_this!}, only: :process
|
351
|
+
|
352
|
+
on_error SomeAppError, ensure: ->{self.always_run_this!} do |ex|
|
353
|
+
# SomeAppError and its subclasses will be rescued and this block will run.
|
354
|
+
# The ensure proc will be run in the ensure block.
|
355
|
+
logger.debug "Couldn't complete transition: #{transition_context.event} because: #{ex.message}"
|
356
|
+
end
|
357
|
+
|
358
|
+
workflow do
|
359
|
+
state :initial do
|
360
|
+
on :process, to: :processing
|
361
|
+
on :different_process, to: :processing
|
362
|
+
end
|
363
|
+
state :processing do
|
364
|
+
on :finish, to: :done
|
365
|
+
end
|
366
|
+
state :done
|
367
|
+
end
|
368
|
+
end
|
369
|
+
```
|
370
|
+
|
371
|
+
## Ensuring code will run
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
|
375
|
+
# This will happen no matter what, whenever the process! event is run.
|
376
|
+
ensure_after_transitions only: :process do
|
377
|
+
self.messages << :foo
|
378
|
+
end
|
379
|
+
|
380
|
+
ensure_after_transitions :clean_up_resources!
|
381
|
+
|
382
|
+
```
|
181
383
|
|
182
384
|
## Conditional Validations
|
183
385
|
|
@@ -188,26 +390,35 @@ Inside the same Article class which was begun above, the following three
|
|
188
390
|
validations would all run when the `submit` event is used to transition
|
189
391
|
from `new` to `awaiting_review`.
|
190
392
|
|
191
|
-
|
192
|
-
|
193
|
-
|
393
|
+
```ruby
|
394
|
+
validates :title, presence: true, if: :transitioning_to_awaiting_review?
|
395
|
+
validates :body, presence: true, if: :transitioning_from_new?
|
396
|
+
validates :author, presence: true, if: :transitioning_via_event_submit?
|
397
|
+
```
|
194
398
|
|
195
399
|
### Halting if validations fail
|
196
400
|
|
197
401
|
# This will create a transition callback which will stop the event
|
198
402
|
# and return false if validations fail.
|
403
|
+
|
199
404
|
halt_transition_unless_valid!
|
200
405
|
|
201
|
-
# This is the same as
|
406
|
+
# This is the same as doing
|
407
|
+
|
408
|
+
before_transition do
|
409
|
+
throw :abort unless valid?
|
410
|
+
end
|
202
411
|
|
203
412
|
### Checking A Transition
|
204
413
|
|
205
414
|
Call `can_transition?` to determine whether the validations would pass if a
|
206
415
|
given event was called:
|
207
416
|
|
208
|
-
|
209
|
-
|
210
|
-
|
417
|
+
```ruby
|
418
|
+
if article.can_transition?(:submit)
|
419
|
+
# Do something interesting
|
420
|
+
end
|
421
|
+
```
|
211
422
|
|
212
423
|
# Transition Context
|
213
424
|
|
@@ -219,65 +430,62 @@ for information about the current transition. See [Workflow::TransitionContext]
|
|
219
430
|
If you will normally call each of your events with the same arguments, the following
|
220
431
|
will help:
|
221
432
|
|
222
|
-
|
223
|
-
|
433
|
+
```ruby
|
434
|
+
class Article < ApplicationRecord
|
435
|
+
include Workflow
|
224
436
|
|
225
|
-
|
437
|
+
before_transition :check_reviewer
|
226
438
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
439
|
+
def check_reviewer
|
440
|
+
# Ability is a class from the cancan gem: https://github.com/CanCanCommunity/cancancan
|
441
|
+
halt('Access denied') unless Ability.new(transition_context.reviewer).can?(:review, self)
|
442
|
+
end
|
231
443
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
end
|
237
|
-
state :reviewed
|
238
|
-
end
|
444
|
+
workflow do
|
445
|
+
event_args :reviewer, :reviewed_at
|
446
|
+
state :new do
|
447
|
+
on :review, to: :reviewed
|
239
448
|
end
|
240
|
-
|
449
|
+
state :reviewed
|
450
|
+
end
|
451
|
+
end
|
452
|
+
```
|
241
453
|
|
242
454
|
Transition event handler
|
243
455
|
------------------------
|
244
456
|
|
245
|
-
|
246
|
-
method with the same name as the event. Then it is automatically invoked
|
457
|
+
You can define a method with the same name as the event. Then it is automatically invoked
|
247
458
|
when event is raised. For the Article workflow defined earlier it would
|
248
459
|
be:
|
249
460
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
461
|
+
```ruby
|
462
|
+
class Article
|
463
|
+
def reject
|
464
|
+
puts 'sending email to the author explaining the reason...'
|
465
|
+
end
|
466
|
+
end
|
467
|
+
```
|
255
468
|
|
256
469
|
`article.review!; article.reject!` will cause state transition to
|
257
470
|
`being_reviewed` state, persist the new state (if integrated with
|
258
471
|
ActiveRecord), invoke this user defined `reject` method and finally
|
259
472
|
persist the `rejected` state.
|
260
473
|
|
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
474
|
|
268
475
|
You can also define event handler accepting/requiring additional
|
269
476
|
arguments:
|
270
477
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
article2 = Article.new
|
278
|
-
article2.submit!
|
279
|
-
article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
|
478
|
+
```ruby
|
479
|
+
class Article
|
480
|
+
def review(reviewer = '')
|
481
|
+
puts "[#{reviewer}] is now reviewing the article"
|
482
|
+
end
|
483
|
+
end
|
280
484
|
|
485
|
+
article2 = Article.new
|
486
|
+
article2.submit!
|
487
|
+
article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
|
488
|
+
```
|
281
489
|
|
282
490
|
Integration with ActiveRecord
|
283
491
|
-----------------------------
|
@@ -286,12 +494,14 @@ Workflow library can handle the state persistence fully automatically. You
|
|
286
494
|
only need to define a string field on the table called `workflow_state`
|
287
495
|
and include the workflow mixin in your model class as usual:
|
288
496
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
497
|
+
```ruby
|
498
|
+
class Order < ActiveRecord::Base
|
499
|
+
include Workflow
|
500
|
+
workflow do
|
501
|
+
# list states and transitions here
|
502
|
+
end
|
503
|
+
end
|
504
|
+
```
|
295
505
|
|
296
506
|
On a database record loading all the state check methods e.g.
|
297
507
|
`article.state`, `article.awaiting_review?` are immediately available.
|
@@ -311,19 +521,21 @@ method.
|
|
311
521
|
Workflow library also adds automatically generated scopes with names based on
|
312
522
|
states names:
|
313
523
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
524
|
+
```ruby
|
525
|
+
class Order < ActiveRecord::Base
|
526
|
+
include Workflow
|
527
|
+
workflow do
|
528
|
+
state :approved
|
529
|
+
state :pending
|
530
|
+
end
|
531
|
+
end
|
321
532
|
|
322
|
-
|
323
|
-
|
533
|
+
# returns all orders with `approved` state
|
534
|
+
Order.with_approved_state
|
324
535
|
|
325
|
-
|
326
|
-
|
536
|
+
# returns all orders with `pending` state
|
537
|
+
Order.with_pending_state
|
538
|
+
```
|
327
539
|
|
328
540
|
### Wrap State Transition in a locking transaction
|
329
541
|
|
@@ -331,39 +543,25 @@ Wrap your transition in a locking transaction to ensure that any exceptions
|
|
331
543
|
raised later in the transition sequence will roll back earlier changes made to
|
332
544
|
the record:
|
333
545
|
|
334
|
-
|
335
|
-
|
336
|
-
|
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:
|
546
|
+
```ruby
|
547
|
+
class Order < ActiveRecord::Base
|
548
|
+
include Workflow
|
346
549
|
|
347
|
-
|
348
|
-
|
349
|
-
:if => :sufficient_battery_level?
|
550
|
+
wrap_transition_in_transaction!
|
551
|
+
# which is the same as the following:
|
350
552
|
|
351
|
-
|
352
|
-
|
553
|
+
around_transition do |model, transition|
|
554
|
+
model.with_lock do
|
555
|
+
transition.call
|
353
556
|
end
|
557
|
+
end
|
354
558
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
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
|
-
|
559
|
+
workflow do
|
560
|
+
state :approved
|
561
|
+
state :pending
|
562
|
+
end
|
563
|
+
end
|
564
|
+
```
|
367
565
|
|
368
566
|
Accessing your workflow specification
|
369
567
|
-------------------------------------
|
@@ -371,34 +569,31 @@ Accessing your workflow specification
|
|
371
569
|
You can easily reflect on workflow specification programmatically - for
|
372
570
|
the whole class or for the current object. Examples:
|
373
571
|
|
374
|
-
|
375
|
-
|
572
|
+
```ruby
|
573
|
+
article2.current_state.events # lists possible events from here
|
376
574
|
|
377
|
-
|
378
|
-
|
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
|
575
|
+
Article.workflow_spec.states.map &:name
|
576
|
+
#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
|
385
577
|
|
578
|
+
# list all events for all states
|
579
|
+
Article.workflow_spec.states.map(&:events).flatten
|
580
|
+
```
|
386
581
|
|
387
582
|
You can also store and later retrieve additional meta data for every
|
388
583
|
state and every event:
|
389
584
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
585
|
+
```ruby
|
586
|
+
class MyProcess
|
587
|
+
include Workflow
|
588
|
+
workflow do
|
589
|
+
state :main, meta: {importance: 8} do
|
590
|
+
on :change, to: :supplemental, meta: {whatever: true}
|
396
591
|
end
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
592
|
+
state :supplemental, meta: {importance: 1}
|
593
|
+
end
|
594
|
+
end
|
595
|
+
puts MyProcess.workflow_spec.find_state(:supplemental).meta[:importance] # => 1
|
596
|
+
```
|
402
597
|
|
403
598
|
Earlier versions
|
404
599
|
----------------
|
data/lib/workflow.rb
CHANGED
@@ -51,7 +51,7 @@ module Workflow
|
|
51
51
|
res || workflow_spec.initial_state
|
52
52
|
end
|
53
53
|
|
54
|
-
# Deprecated. Check for false return value from {#
|
54
|
+
# Deprecated. Check for false return value from {#transition!}
|
55
55
|
# @return true if the last transition was halted by one of the transition callbacks.
|
56
56
|
def halted?
|
57
57
|
@halted
|
@@ -66,7 +66,7 @@ module Workflow
|
|
66
66
|
# @param [Symbol] name name of event to initiate
|
67
67
|
# @param [Mixed] *args Arguments passed to state transition. Available also to callbacks
|
68
68
|
# @return [Type] description of returned object
|
69
|
-
def
|
69
|
+
def transition!(name, *args, **attributes)
|
70
70
|
name = name.to_sym
|
71
71
|
event = current_state.find_event(name)
|
72
72
|
raise NoTransitionAllowed.new(
|
@@ -95,7 +95,7 @@ module Workflow
|
|
95
95
|
run_all_callbacks do
|
96
96
|
callback_value = run_action_callback name, *args
|
97
97
|
persist_value = persist_workflow_state(target.name)
|
98
|
-
callback_value || persist_value
|
98
|
+
return_value = callback_value || persist_value
|
99
99
|
end
|
100
100
|
ensure
|
101
101
|
@transition_context = nil
|
@@ -264,7 +264,7 @@ module Workflow
|
|
264
264
|
event_name = event.name
|
265
265
|
module_eval do
|
266
266
|
define_method "#{event_name}!".to_sym do |*args|
|
267
|
-
|
267
|
+
transition!(event_name, *args)
|
268
268
|
end
|
269
269
|
|
270
270
|
define_method "can_#{event_name}?" do
|
@@ -5,6 +5,10 @@ module Workflow
|
|
5
5
|
module ActiveRecordValidations
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
included do
|
9
|
+
prepend RevalidateOnlyAfterAttributesChange
|
10
|
+
end
|
11
|
+
|
8
12
|
###
|
9
13
|
#
|
10
14
|
# Captures instance method calls of the form `:transitioning_from_<state_name>`
|
@@ -25,14 +29,6 @@ module Workflow
|
|
25
29
|
end
|
26
30
|
end
|
27
31
|
|
28
|
-
def valid?(context=nil)
|
29
|
-
if errors.any?
|
30
|
-
false
|
31
|
-
else
|
32
|
-
super
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
32
|
def can_transition?(event_id)
|
37
33
|
event = current_state.find_event(event_id)
|
38
34
|
return false unless event
|
@@ -43,8 +39,10 @@ module Workflow
|
|
43
39
|
return false unless to
|
44
40
|
|
45
41
|
within_transition(from, to.name, event_id) do
|
46
|
-
valid?
|
42
|
+
return valid?
|
47
43
|
end
|
44
|
+
ensure
|
45
|
+
errors.clear
|
48
46
|
end
|
49
47
|
|
50
48
|
###
|
@@ -78,6 +76,37 @@ module Workflow
|
|
78
76
|
end
|
79
77
|
end
|
80
78
|
|
79
|
+
# Override for ActiveRecord::Validations#valid?
|
80
|
+
# Since we are validating inside of a transition,
|
81
|
+
# We need to be able to maintain the errors list for the object
|
82
|
+
# through future valid? calls or the list will be cleared
|
83
|
+
# next time valid? is called.
|
84
|
+
#
|
85
|
+
# Once any attributes have changed on the object, the following call to {#valid?}
|
86
|
+
# will cause revalidation.
|
87
|
+
#
|
88
|
+
# Note that a change on an association will not trigger a reset,
|
89
|
+
# meaning that one should call `errors.clear` before {#valid?} will
|
90
|
+
# trigger validations to run anew.
|
91
|
+
module RevalidateOnlyAfterAttributesChange
|
92
|
+
def valid?(context=nil)
|
93
|
+
if errors.any? && !@changed_since_validation
|
94
|
+
false
|
95
|
+
else
|
96
|
+
begin
|
97
|
+
return super
|
98
|
+
ensure
|
99
|
+
@changed_since_validation = false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def write_attribute(attr_name, value)
|
105
|
+
@changed_since_validation = true
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
81
110
|
module ClassMethods
|
82
111
|
def halt_transition_unless_valid!
|
83
112
|
before_transition unless: :valid? do |model|
|
data/lib/workflow/callbacks.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'workflow/callbacks/callback'
|
2
|
+
require 'workflow/callbacks/transition_callback_wrapper'
|
3
|
+
|
1
4
|
module Workflow
|
2
5
|
module Callbacks
|
3
6
|
|
@@ -23,6 +26,42 @@ module Workflow
|
|
23
26
|
end
|
24
27
|
|
25
28
|
module ClassMethods
|
29
|
+
def ensure_after_transitions(name=nil, **opts, &block)
|
30
|
+
_ensure_procs = [name, block].compact.map do |exe|
|
31
|
+
Callback.new(exe)
|
32
|
+
end
|
33
|
+
|
34
|
+
prepend_around_transition **opts do |obj, callbacks|
|
35
|
+
begin
|
36
|
+
callbacks.call()
|
37
|
+
ensure
|
38
|
+
_ensure_procs.each {|l| l.callback.call obj, ->{}}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_error(error_class=Exception, **opts, &block)
|
44
|
+
_error_procs = [opts.delete(:rescue)].compact.map do |exe|
|
45
|
+
Callback.new(exe)
|
46
|
+
end
|
47
|
+
|
48
|
+
_ensure_procs = [opts.delete(:ensure)].compact.map do |exe|
|
49
|
+
Callback.new(exe)
|
50
|
+
end
|
51
|
+
|
52
|
+
prepend_around_transition **opts do |obj, callbacks|
|
53
|
+
begin
|
54
|
+
callbacks.call
|
55
|
+
rescue error_class => ex
|
56
|
+
self.instance_exec(ex, &block) if block_given?
|
57
|
+
# block.call(ex) if block_given?
|
58
|
+
_error_procs.each {|l| l.callback.call self, ->{}}
|
59
|
+
ensure
|
60
|
+
_ensure_procs.each {|l| l.callback.call self, ->{}}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
26
65
|
##
|
27
66
|
# @!method before_transition
|
28
67
|
#
|
@@ -207,13 +246,13 @@ module Workflow
|
|
207
246
|
CALLBACK_MAP.each do |type, context_attribute|
|
208
247
|
define_method "#{callback}_#{type}" do |*names, &blk|
|
209
248
|
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
210
|
-
set_callback(type, callback, name, options)
|
249
|
+
set_callback(type, callback, TransitionCallbackWrapper.build_wrapper(callback, name), options)
|
211
250
|
end
|
212
251
|
end
|
213
252
|
|
214
253
|
define_method "prepend_#{callback}_#{type}" do |*names, &blk|
|
215
254
|
_insert_callbacks(names, context_attribute, blk) do |name, options|
|
216
|
-
set_callback(type, callback, name, options.merge(prepend: true))
|
255
|
+
set_callback(type, callback, TransitionCallbackWrapper.build_wrapper(callback, name), options.merge(prepend: true))
|
217
256
|
end
|
218
257
|
end
|
219
258
|
|
@@ -221,7 +260,7 @@ module Workflow
|
|
221
260
|
# for details on the allowed parameters.
|
222
261
|
define_method "skip_#{callback}_#{type}" do |*names|
|
223
262
|
_insert_callbacks(names, context_attribute) do |name, options|
|
224
|
-
skip_callback(type, callback, name, options)
|
263
|
+
skip_callback(type, callback, TransitionCallbackWrapper.build_wrapper(callback, name), options)
|
225
264
|
end
|
226
265
|
end
|
227
266
|
|
@@ -230,6 +269,16 @@ module Workflow
|
|
230
269
|
end
|
231
270
|
end
|
232
271
|
|
272
|
+
def applicable_callback?(callback, procedure)
|
273
|
+
arity = procedure.arity
|
274
|
+
return true if arity < 0 || arity > 2
|
275
|
+
if [:key, :keyreq, :keyrest].include? procedure.parameters[-1][0]
|
276
|
+
return true
|
277
|
+
else
|
278
|
+
return false
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
233
282
|
private
|
234
283
|
def _insert_callbacks(callbacks, context_attribute, block = nil)
|
235
284
|
options = callbacks.extract_options!
|
@@ -259,6 +308,7 @@ module Workflow
|
|
259
308
|
private
|
260
309
|
# TODO: Do something here.
|
261
310
|
def halted_callback_hook(filter)
|
311
|
+
# byebug
|
262
312
|
end
|
263
313
|
|
264
314
|
def run_all_callbacks(&block)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Workflow
|
2
|
+
module Callbacks
|
3
|
+
#
|
4
|
+
# Receives an expression and generates a lambda that can be called against
|
5
|
+
# an object, for use in callback logic.
|
6
|
+
#
|
7
|
+
# Adapted from ActiveSupport::Callbacks
|
8
|
+
# https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L296
|
9
|
+
class Callback
|
10
|
+
include Comparable
|
11
|
+
|
12
|
+
attr_reader :expression, :callback
|
13
|
+
def initialize(expression, inverted=false)
|
14
|
+
@expression = expression
|
15
|
+
@callback = make_lambda(expression)
|
16
|
+
if inverted
|
17
|
+
@callback = invert_lambda(@callback)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(target)
|
22
|
+
callback.call(target, ->{})
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.build_inverse(expression)
|
26
|
+
new expression, true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def invert_lambda(l)
|
32
|
+
lambda { |*args, &blk| !l.call(*args, &blk) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Filters support:
|
36
|
+
#
|
37
|
+
# Symbols:: A method to call.
|
38
|
+
# Strings:: Some content to evaluate.
|
39
|
+
# Procs:: A proc to call with the object.
|
40
|
+
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
|
41
|
+
#
|
42
|
+
# All of these objects are converted into a lambda and handled
|
43
|
+
# the same after this point.
|
44
|
+
def make_lambda(filter)
|
45
|
+
case filter
|
46
|
+
when Symbol
|
47
|
+
lambda { |target, _, &blk| target.send filter, &blk }
|
48
|
+
when String
|
49
|
+
l = eval "lambda { |value| #{filter} }"
|
50
|
+
lambda { |target, value| target.instance_exec(value, &l) }
|
51
|
+
# when Conditionals::Value then filter
|
52
|
+
when ::Proc
|
53
|
+
if filter.arity > 1
|
54
|
+
return lambda { |target, _, &block|
|
55
|
+
raise ArgumentError unless block
|
56
|
+
target.instance_exec(target, block, &filter)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
if filter.arity <= 0
|
61
|
+
lambda { |target, _| target.instance_exec(&filter) }
|
62
|
+
else
|
63
|
+
lambda { |target, _| target.instance_exec(target, &filter) }
|
64
|
+
end
|
65
|
+
else
|
66
|
+
scopes = Array(chain_config[:scope])
|
67
|
+
method_to_call = scopes.map{ |s| public_send(s) }.join("_")
|
68
|
+
|
69
|
+
lambda { |target, _, &blk|
|
70
|
+
filter.public_send method_to_call, target, &blk
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def compute_identifier(filter)
|
76
|
+
case filter
|
77
|
+
when String, ::Proc
|
78
|
+
filter.object_id
|
79
|
+
else
|
80
|
+
filter
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Workflow
|
2
|
+
module Callbacks
|
3
|
+
class TransitionCallbackWrapper
|
4
|
+
attr_reader :callback_type, :raw_proc
|
5
|
+
def initialize(callback_type, raw_proc)
|
6
|
+
@callback_type = callback_type
|
7
|
+
@raw_proc = raw_proc
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.build_wrapper(callback_type, raw_proc)
|
11
|
+
if raw_proc.kind_of? ::Proc
|
12
|
+
new(callback_type, raw_proc).wrapper
|
13
|
+
else
|
14
|
+
raw_proc
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def wrapper
|
19
|
+
arguments = [
|
20
|
+
name_arguments_string,
|
21
|
+
rest_param_string,
|
22
|
+
kw_arguments_string,
|
23
|
+
keyrest_string,
|
24
|
+
procedure_string].compact.join(', ')
|
25
|
+
|
26
|
+
raw_proc = self.raw_proc
|
27
|
+
str = <<-EOF
|
28
|
+
Proc.new do |target#{', callbacks' if around_callback?}|
|
29
|
+
attributes = transition_context.attributes.dup
|
30
|
+
target.instance_exec(#{arguments})
|
31
|
+
end
|
32
|
+
EOF
|
33
|
+
eval(str)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def around_callback?
|
39
|
+
callback_type == :around
|
40
|
+
end
|
41
|
+
|
42
|
+
def name_arguments_string
|
43
|
+
params = name_params
|
44
|
+
names = []
|
45
|
+
names << 'target' if params.shift
|
46
|
+
(names << 'callbacks') && params.shift if around_callback?
|
47
|
+
names += params.map{|t| "transition_context.#{t}"}
|
48
|
+
return names.join(', ') if names.any?
|
49
|
+
end
|
50
|
+
|
51
|
+
def kw_arguments_string
|
52
|
+
params = kw_params.map do |name|
|
53
|
+
"#{name}: attributes.delete(#{name.inspect})"
|
54
|
+
end
|
55
|
+
params.join(', ') if params.any?
|
56
|
+
end
|
57
|
+
|
58
|
+
def keyrest_string
|
59
|
+
'**attributes' if keyrest_param
|
60
|
+
end
|
61
|
+
|
62
|
+
def rest_param_string
|
63
|
+
'*transition_context.event_args' if rest_param
|
64
|
+
end
|
65
|
+
|
66
|
+
def procedure_string
|
67
|
+
'&raw_proc'
|
68
|
+
end
|
69
|
+
|
70
|
+
def name_params
|
71
|
+
params_by_type :opt, :req
|
72
|
+
end
|
73
|
+
|
74
|
+
def kw_params
|
75
|
+
params_by_type :keyreq, :keyopt
|
76
|
+
end
|
77
|
+
|
78
|
+
def keyrest_param
|
79
|
+
params_by_type(:keyrest).first
|
80
|
+
end
|
81
|
+
|
82
|
+
def rest_param
|
83
|
+
params_by_type(:rest).first
|
84
|
+
end
|
85
|
+
|
86
|
+
def params_by_type(*types)
|
87
|
+
parameters.select do |type, name|
|
88
|
+
types.include? type
|
89
|
+
end.map(&:last)
|
90
|
+
end
|
91
|
+
|
92
|
+
def parameters
|
93
|
+
raw_proc.parameters
|
94
|
+
end
|
95
|
+
def arity
|
96
|
+
raw_proc.arity
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/workflow/event.rb
CHANGED
@@ -44,7 +44,6 @@ module Workflow
|
|
44
44
|
end
|
45
45
|
|
46
46
|
class Conditions #:nodoc:#
|
47
|
-
|
48
47
|
def initialize(**options, &block)
|
49
48
|
@if = Array(options[:if])
|
50
49
|
@unless = Array(options[:unless])
|
@@ -57,72 +56,15 @@ module Workflow
|
|
57
56
|
end
|
58
57
|
|
59
58
|
def apply?(target)
|
60
|
-
|
61
|
-
@conditions_lambdas.all?{|l| l.call(target, ->(){})}
|
59
|
+
@conditions_lambdas.all?{|l| l.call(target)}
|
62
60
|
end
|
63
61
|
|
64
62
|
private
|
65
63
|
|
66
|
-
# Copied from https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L366
|
67
|
-
def invert_lambda(l)
|
68
|
-
lambda { |*args, &blk| !l.call(*args, &blk) }
|
69
|
-
end
|
70
|
-
|
71
|
-
# Filters support:
|
72
|
-
#
|
73
|
-
# Symbols:: A method to call.
|
74
|
-
# Strings:: Some content to evaluate.
|
75
|
-
# Procs:: A proc to call with the object.
|
76
|
-
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
|
77
|
-
#
|
78
|
-
# All of these objects are converted into a lambda and handled
|
79
|
-
# the same after this point.
|
80
|
-
# Copied from https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L379
|
81
|
-
def make_lambda(filter)
|
82
|
-
case filter
|
83
|
-
when Symbol
|
84
|
-
lambda { |target, _, &blk| target.send filter, &blk }
|
85
|
-
when String
|
86
|
-
l = eval "lambda { |value| #{filter} }"
|
87
|
-
lambda { |target, value| target.instance_exec(value, &l) }
|
88
|
-
# when Conditionals::Value then filter
|
89
|
-
when ::Proc
|
90
|
-
if filter.arity > 1
|
91
|
-
return lambda { |target, _, &block|
|
92
|
-
raise ArgumentError unless block
|
93
|
-
target.instance_exec(target, block, &filter)
|
94
|
-
}
|
95
|
-
end
|
96
|
-
|
97
|
-
if filter.arity <= 0
|
98
|
-
lambda { |target, _| target.instance_exec(&filter) }
|
99
|
-
else
|
100
|
-
lambda { |target, _| target.instance_exec(target, &filter) }
|
101
|
-
end
|
102
|
-
else
|
103
|
-
scopes = Array(chain_config[:scope])
|
104
|
-
method_to_call = scopes.map{ |s| public_send(s) }.join("_")
|
105
|
-
|
106
|
-
lambda { |target, _, &blk|
|
107
|
-
filter.public_send method_to_call, target, &blk
|
108
|
-
}
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
# From https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L410
|
113
|
-
def compute_identifier(filter)
|
114
|
-
case filter
|
115
|
-
when String, ::Proc
|
116
|
-
filter.object_id
|
117
|
-
else
|
118
|
-
filter
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
64
|
# From https://github.com/rails/rails/blob/bca2e69b785fa3cdbe148b0d2dd5d3b58f6daf53/activesupport/lib/active_support/callbacks.rb#L419
|
123
65
|
def conditions_lambdas
|
124
|
-
@if.map { |c|
|
125
|
-
@unless.map { |c|
|
66
|
+
@if.map { |c| Callbacks::Callback.new c } +
|
67
|
+
@unless.map { |c| Callbacks::Callback.new c, true }
|
126
68
|
end
|
127
69
|
end
|
128
70
|
end
|
@@ -42,7 +42,7 @@ module Workflow
|
|
42
42
|
state.events.each do |event|
|
43
43
|
event.transitions.each do |transition|
|
44
44
|
target_state = spec.find_state(transition.target_state)
|
45
|
-
|
45
|
+
if target_state.nil?
|
46
46
|
raise Workflow::Errors::WorkflowDefinitionError.new("Event #{event.name} transitions to #{transition.target_state} but there is no such state.")
|
47
47
|
end
|
48
48
|
transition.target_state = target_state
|
@@ -117,9 +117,9 @@ module Workflow
|
|
117
117
|
# end
|
118
118
|
#
|
119
119
|
# a = Article.new
|
120
|
-
# a.
|
120
|
+
# a.transition! :foo
|
121
121
|
# a.current_state.name # => :bax
|
122
|
-
# a.
|
122
|
+
# a.transition! :revert_bar
|
123
123
|
# a.current_state.name # => :foo
|
124
124
|
#```
|
125
125
|
def define_revert_events!
|
data/lib/workflow/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-workflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.4.1.
|
4
|
+
version: 1.4.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tyler Gannon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -219,6 +219,8 @@ files:
|
|
219
219
|
- lib/workflow/adapters/active_record_validations.rb
|
220
220
|
- lib/workflow/adapters/remodel.rb
|
221
221
|
- lib/workflow/callbacks.rb
|
222
|
+
- lib/workflow/callbacks/callback.rb
|
223
|
+
- lib/workflow/callbacks/transition_callback_wrapper.rb
|
222
224
|
- lib/workflow/configuration.rb
|
223
225
|
- lib/workflow/draw.rb
|
224
226
|
- lib/workflow/errors.rb
|