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 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