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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ec4b025de0b47929f1e64a9315b8da9d282785ef
4
- data.tar.gz: 2ffc02aae267a096d51c4d7c531f94f95abf87ab
3
+ metadata.gz: 8270a626780c254a58897c2cef2e0b162b5fbcae
4
+ data.tar.gz: 1a9e4480f3306fc8b10136d2dce4c92d3570b529
5
5
  SHA512:
6
- metadata.gz: daabd4600fb5fbda2fc576b7cc520f411ac137baeeeae5e28248229b2e81c0554a6e97190c5f4b1e64413409cc713407c1fe0d27b897f2aacdc993579117c157
7
- data.tar.gz: 58f77cc643f6418d91a43b07d89b253cacfca3cb872a033bb49417e6ed333faa9d14441b32e97e828485f6ec69a0365f2a5449cbeee442157eadeef46d4e7a22
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 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.
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
- The primary difference here is the use of [ActiveSupport::Callbacks](http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html)
8
+ * Use of [ActiveSupport::Callbacks](http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html)
10
9
  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.
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
- You can also take advantage of ActiveRecord's conditional validation syntax,
19
- to apply validations only to specific state transitions.
14
+ ## Installation
20
15
 
16
+ gem install rails-workflow
21
17
 
22
- Installation
23
- ------------
18
+ ## Configuration
24
19
 
25
- gem install rails-workflow
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. ;) Time to upgrade.
32
-
36
+ I've only tested with Ruby 2.3. ;)
33
37
 
34
38
  # Basic workflow definition:
35
39
 
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
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
- 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{}
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
- 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
-
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
- Callbacks with this strategy used the same as [ActionController Callbacks](http://guides.rubyonrails.org/action_controller_overview.html#filters).
166
+ Three classes of callbacks:
88
167
 
89
- You can configure any number of `before`, `around`, or `after` transition callbacks.
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
- `before_transition` and `around_transition` are called in the order they are set,
92
- and `after_transition` callbacks are called in reverse order.
176
+ Callbacks run in this order:
93
177
 
94
- ## Around Transition
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
- Allows you to run code surrounding the state transition.
186
+ Within each group, the callbacks fire in the order they are set.
97
187
 
98
- around_transition :wrap_in_transaction
188
+ ### Halting callbacks
189
+ Inside any `:before` callback, you can halt the callback chain:
99
190
 
100
- def wrap_in_transaction(&block)
101
- Article.transaction(&block)
102
- end
191
+ ```ruby
192
+ before_enter do
193
+ throw :abort
194
+ end
195
+ ```
103
196
 
104
- You can also define the callback using a block:
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
- around_transition do |object, transition|
107
- object.with_lock do
108
- transition.call
109
- end
110
- end
200
+ ## Around Transition
111
201
 
112
- ### Replacement for workflow's `on_error` proc:
202
+ Allows you to run code surrounding the state transition.
113
203
 
114
- around_transition :catch_errors
204
+ ```ruby
205
+ around_transition :wrap_in_transaction
115
206
 
116
- def catch_errors
117
- begin
118
- yield
119
- rescue SomeApplicationError => ex
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
- before_transition :check_title
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
- prepend_before_transition :some_before_transition
156
- prepend_around_transition :some_around_transition
157
- prepend_after_transition :some_after_transition
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
- before_transition :do_something, if: :valid?
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 will run `if` or `unless` the event being processed is in the list given
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
- # Run this callback only on the `accept` and `publish` events.
177
- before_transition :do_something, only: [:accept, :publish]
336
+ ```ruby
337
+ class WorkflowModel
338
+ include Workflow
178
339
 
179
- # Run this callback on events other than the `accept` and `publish` events.
180
- before_transition :do_something_else, except: [:accept, :publish]
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
- 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?
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
- if article.can_transition?(:submit)
209
- # Do something interesting
210
- end
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
- class Article < ApplicationRecord
223
- include Workflow
433
+ ```ruby
434
+ class Article < ApplicationRecord
435
+ include Workflow
224
436
 
225
- before_transition :check_reviewer
437
+ before_transition :check_reviewer
226
438
 
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
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
- 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
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
- 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
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
- class Article
251
- def reject
252
- puts 'sending email to the author explaining the reason...'
253
- end
254
- end
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
- 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
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
- class Order < ActiveRecord::Base
290
- include Workflow
291
- workflow do
292
- # list states and transitions here
293
- end
294
- end
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
- class Order < ActiveRecord::Base
315
- include Workflow
316
- workflow do
317
- state :approved
318
- state :pending
319
- end
320
- end
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
- # returns all orders with `approved` state
323
- Order.with_approved_state
533
+ # returns all orders with `approved` state
534
+ Order.with_approved_state
324
535
 
325
- # returns all orders with `pending` state
326
- Order.with_pending_state
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
- 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:
546
+ ```ruby
547
+ class Order < ActiveRecord::Base
548
+ include Workflow
346
549
 
347
- state :off
348
- event :turn_on, :transition_to => :on,
349
- :if => :sufficient_battery_level?
550
+ wrap_transition_in_transaction!
551
+ # which is the same as the following:
350
552
 
351
- event :turn_on, :transition_to => :low_battery,
352
- :if => proc { |device| device.battery_level > 0 }
553
+ around_transition do |model, transition|
554
+ model.with_lock do
555
+ transition.call
353
556
  end
557
+ end
354
558
 
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
-
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
- article2.current_state.events # lists possible events from here
375
- article2.current_state.events[:reject].transitions_to # => :rejected
572
+ ```ruby
573
+ article2.current_state.events # lists possible events from here
376
574
 
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
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
- class MyProcess
391
- include Workflow
392
- workflow do
393
- state :main, :meta => {:importance => 8}
394
- state :supplemental, :meta => {:importance => 1}
395
- end
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
- 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
-
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 {#process_event!}
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 process_event!(name, *args, **attributes)
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
- process_event!(event_name, *args)
267
+ transition!(event_name, *args)
268
268
  end
269
269
 
270
270
  define_method "can_#{event_name}?" do
@@ -22,6 +22,7 @@ module Workflow
22
22
  attrs[:updated_at] = DateTime.now
23
23
  end
24
24
  update_columns attrs
25
+ new_value
25
26
  else
26
27
  self[self.class.workflow_column] = new_value
27
28
  end
@@ -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|
@@ -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
@@ -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
- # TODO: Remove the second parameter from the conditions below.
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| make_lambda c } +
125
- @unless.map { |c| invert_lambda make_lambda 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
- unless target_state.present?
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.process_event! :foo
120
+ # a.transition! :foo
121
121
  # a.current_state.name # => :bax
122
- # a.process_event! :revert_bar
122
+ # a.transition! :revert_bar
123
123
  # a.current_state.name # => :foo
124
124
  #```
125
125
  def define_revert_events!
@@ -1,3 +1,3 @@
1
1
  module Workflow
2
- VERSION = "1.4.1.2"
2
+ VERSION = "1.4.1.3"
3
3
  end
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.2
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-22 00:00:00.000000000 Z
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