rails-workflow 1.4.1.2 → 1.4.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|