workflow 2.0.2 → 3.0.0

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
  SHA256:
3
- metadata.gz: 4834f20238e73dbf0304747da6f098612e395c92a6b839908c7c0a613ad272b8
4
- data.tar.gz: 40a8fa57362ed445be012847fe283db2b417225ea3db4ea4c02bcad957630b57
3
+ metadata.gz: a90286a0749bd94cabc4fab8cc89dc5c9fc7b05d60ea5cf58cd6493852159206
4
+ data.tar.gz: 8c4015a8ba91be4c4fc37bd5e8004534146d1daedfd290d32538edec33ac02a3
5
5
  SHA512:
6
- metadata.gz: 9cb4cd4a9738529cdffc867d04716dbe0bce0df361e84fe55570563624441728e41d2bfec5639cbae6e64dbcf86f047abd73adb542140cb0ecdf92f92be39237
7
- data.tar.gz: 1cbbddb5014d83e9ace2b03e2d5e60cf01acbc8f1579d942b68267d144b3513f01fb23797219450e44aca85ae476e4675d57e97fcb149043be16d951c17608c1
6
+ metadata.gz: b72b7e1d8de350a9e5478d291d7ff6a1e0f6f192bff31eac6951f9f5e8fbf4941597e84e990838402902fb65fa79acabfffdfc5fd168d937967e5d63239dc7f1
7
+ data.tar.gz: 9aaea40918c7f2b55b9e51a8ae881708869af77e22995545a5e5017731b19575d7f0ed0a6ffbd5343a6c8883854d81e83749e7447abedd796d6c2d77345bebf8
data/README.adoc ADDED
@@ -0,0 +1,564 @@
1
+ :doctype: book
2
+ :toc: macro
3
+ :toclevels: 1
4
+ :sectlinks:
5
+ :idprefix:
6
+
7
+ # Workflow
8
+
9
+ image:https://img.shields.io/gem/v/workflow.svg[link=https://rubygems.org/gems/workflow]
10
+ image:https://github.com/geekq/workflow/actions/workflows/test.yml/badge.svg[link=https://github.com/geekq/workflow/actions/workflows/test.yml]
11
+ image:https://codeclimate.com/github/geekq/workflow/badges/gpa.svg[link=https://codeclimate.com/github/geekq/workflow]
12
+ image:https://codeclimate.com/github/geekq/workflow/badges/coverage.svg[link=https://codeclimate.com/github/geekq/workflow/coverage]
13
+
14
+ Note: you can find documentation for specific workflow rubygem versions
15
+ at http://rubygems.org/gems/workflow : select a version (optional,
16
+ default is latest release), click "Documentation" link. When reading on
17
+ github.com, the README refers to the upcoming release.
18
+
19
+ toc::[]
20
+
21
+ What is workflow?
22
+ -----------------
23
+
24
+ Workflow is a finite-state-machine-inspired API for modeling and
25
+ interacting with what we tend to refer to as 'workflow'.
26
+
27
+ A lot of business modeling tends to involve workflow-like concepts, and
28
+ the aim of this library is to make the expression of these concepts as
29
+ clear as possible, using similar terminology as found in state machine
30
+ theory.
31
+
32
+ So, a workflow has a state. It can only be in one state at a time. When
33
+ a workflow changes state, we call that a transition. Transitions occur
34
+ on an event, so events cause transitions to occur. Additionally, when an
35
+ event fires, other arbitrary code can be executed, we call those actions.
36
+ So any given state has a bunch of events, any event in a state causes a
37
+ transition to another state and potentially causes code to be executed
38
+ (an action). We can hook into states when they are entered, and exited
39
+ from, and we can cause transitions to fail (guards), and we can hook in
40
+ to every transition that occurs ever for whatever reason we can come up
41
+ with.
42
+
43
+ Now, all that's a mouthful, but we'll demonstrate the API bit by bit
44
+ with a real-ish world example.
45
+
46
+ Let's say we're modeling article submission from journalists. An article
47
+ is written, then submitted. When it's submitted, it's awaiting review.
48
+ Someone reviews the article, and then either accepts or rejects it.
49
+ Here is the expression of this workflow using the API:
50
+
51
+ ```rb
52
+ class Article
53
+ include Workflow
54
+ workflow do
55
+ state :new do
56
+ event :submit, :transitions_to => :awaiting_review
57
+ end
58
+ state :awaiting_review do
59
+ event :review, :transitions_to => :being_reviewed
60
+ end
61
+ state :being_reviewed do
62
+ event :accept, :transitions_to => :accepted
63
+ event :reject, :transitions_to => :rejected
64
+ end
65
+ state :accepted
66
+ state :rejected
67
+ end
68
+ end
69
+ ```
70
+
71
+ Nice, isn't it!
72
+
73
+ Note: the first state in the definition (`:new` in the example, but you
74
+ can name it as you wish) is used as the initial state - newly created
75
+ objects start their life cycle in that state.
76
+
77
+ Let's create an article instance and check in which state it is:
78
+
79
+ ```rb
80
+ article = Article.new
81
+ article.accepted? # => false
82
+ article.new? # => true
83
+ ```
84
+
85
+ You can also access the whole `current_state` object including the list
86
+ of possible events and other meta information:
87
+
88
+ article.current_state
89
+ => #<Workflow::State:0x7f1e3d6731f0 @events={
90
+ :submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
91
+ @transitions_to=:awaiting_review, @name=:submit, @meta={}>},
92
+ name:new, meta{}
93
+
94
+ You can also check, whether a state comes before or after another state (by the
95
+ order they were defined):
96
+
97
+ ```rb
98
+ article.current_state # => being_reviewed
99
+ article.current_state < :accepted # => true
100
+ article.current_state >= :accepted # => false
101
+ article.current_state.between? :awaiting_review, :rejected # => true
102
+ ```
103
+
104
+ Now we can call the submit event, which transitions to the
105
+ <tt>:awaiting_review</tt> state:
106
+
107
+ ```rb
108
+ article.submit!
109
+ article.awaiting_review? # => true
110
+ ```
111
+
112
+ Events are actually instance methods on a workflow, and depending on the
113
+ state you're in, you'll have a different set of events used to
114
+ transition to other states.
115
+
116
+ It is also easy to check, if a certain transition is possible from the
117
+ current state . `article.can_submit?` checks if there is a `:submit`
118
+ event (transition) defined for the current state.
119
+
120
+
121
+ Getting started
122
+ ---------------
123
+
124
+ === Installation
125
+
126
+ ```sh
127
+ gem install workflow
128
+ ```
129
+
130
+ **Important**: If you're interested in graphing your workflow state machine, you will also need to
131
+ install the `activesupport` and `ruby-graphviz` gems.
132
+
133
+ Versions up to and including 1.0.0 are also available as a single file download -
134
+ [lib/workflow.rb file](https://github.com/geekq/workflow/blob/v1.0.0/lib/workflow.rb).
135
+
136
+
137
+ === Examples
138
+
139
+ After installation or downloading the library you can easily try out
140
+ all the example code from this README in irb.
141
+
142
+ $ irb
143
+ require 'rubygems'
144
+ require 'workflow'
145
+
146
+ Now just copy and paste the source code from the beginning of this README
147
+ file snippet by snippet and observe the output.
148
+
149
+
150
+ ### Transition event handler
151
+
152
+ The best way is to use convention over configuration and to define a
153
+ method with the same name as the event. Then it is automatically invoked
154
+ when event is raised. For the Article workflow defined earlier it would
155
+ be:
156
+
157
+ ```rb
158
+ class Article
159
+ def reject
160
+ puts 'sending email to the author explaining the reason...'
161
+ end
162
+ end
163
+ ```
164
+
165
+ `article.review!; article.reject!` will cause state transition to
166
+ `being_reviewed` state, persist the new state (if integrated with
167
+ ActiveRecord), invoke this user defined `reject` method and finally
168
+ persist the `rejected` state.
169
+
170
+ Note: on successful transition from one state to another the workflow
171
+ gem immediately persists the new workflow state with `update_column()`,
172
+ bypassing any ActiveRecord callbacks including `updated_at` update.
173
+ This way it is possible to deal with the validation and to save the
174
+ pending changes to a record at some later point instead of the moment
175
+ when transition occurs.
176
+
177
+ You can also define event handler accepting/requiring additional
178
+ arguments:
179
+
180
+ ```rb
181
+ class Article
182
+ def review(reviewer = '')
183
+ puts "[#{reviewer}] is now reviewing the article"
184
+ end
185
+ end
186
+
187
+ article2 = Article.new
188
+ article2.submit!
189
+ article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
190
+ ```
191
+
192
+ Alternative way is to use a block (only recommended for short event
193
+ implementation without further code nesting):
194
+
195
+ ```rb
196
+ event :review, :transitions_to => :being_reviewed do |reviewer|
197
+ # store the reviewer
198
+ end
199
+ ```
200
+
201
+ We've noticed, that mixing the list of events and states with the blocks
202
+ invoked for particular transitions leads to a bumpy and poorly readable code
203
+ due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
204
+ we decided to invoke an optional user defined callback method with the same
205
+ name as the event (convention over configuration) as explained before.
206
+
207
+ State persistence
208
+ -----------------
209
+
210
+ === ActiveRecord
211
+
212
+ Note: Workflow 2.0 is a major refactoring for the `worklow` library.
213
+ If your application suddenly breaks after the workflow 2.0 release, you've
214
+ probably got your Gemfile wrong ;-). workflow uses
215
+ https://guides.rubygems.org/patterns/#semantic-versioning[semantic versioning].
216
+ For highest compatibility please reference the desired major+minor version.
217
+
218
+ Note on ActiveRecord/Rails 4.\*, 5.\* Support:
219
+
220
+ Since integration with ActiveRecord makes over 90% of the issues and
221
+ maintenance effort, and also to allow for an independent (faster) release cycle
222
+ for Rails support, starting with workflow **version 2.0** in January 2019 the
223
+ support for ActiveRecord (4.\*, 5.\* and newer) has been extracted into a separate
224
+ gem. Read at
225
+ https://github.com/geekq/workflow-activerecord[workflow-activerecord], how to
226
+ include the right gem.
227
+
228
+ To use legacy built-in ActiveRecord 2.3 - 4.* support, reference Workflow 1.2 in
229
+ your Gemfile:
230
+
231
+ gem 'workflow', '~> 1.2'
232
+
233
+
234
+ === Custom workflow state persistence
235
+
236
+ If you do not use a relational database and ActiveRecord, you can still
237
+ integrate the workflow very easily. To implement persistence you just
238
+ need to override `load_workflow_state` and
239
+ `persist_workflow_state(new_value)` methods. Next section contains an example for
240
+ using CouchDB, a document oriented database.
241
+
242
+ http://tim.lossen.de/[Tim Lossen] implemented support
243
+ for http://github.com/tlossen/remodel[remodel] / http://github.com/antirez/redis[redis]
244
+ key-value store.
245
+
246
+ === Integration with CouchDB
247
+
248
+ We are using the compact http://github.com/geekq/couchtiny[couchtiny library]
249
+ here. But the implementation would look similar for the popular
250
+ couchrest library.
251
+
252
+ ```rb
253
+ require 'couchtiny'
254
+ require 'couchtiny/document'
255
+ require 'workflow'
256
+
257
+ class User < CouchTiny::Document
258
+ include Workflow
259
+ workflow do
260
+ state :submitted do
261
+ event :activate_via_link, :transitions_to => :proved_email
262
+ end
263
+ state :proved_email
264
+ end
265
+
266
+ def load_workflow_state
267
+ self[:workflow_state]
268
+ end
269
+
270
+ def persist_workflow_state(new_value)
271
+ self[:workflow_state] = new_value
272
+ save!
273
+ end
274
+ end
275
+ ```
276
+
277
+ Please also have a look at
278
+ http://github.com/geekq/workflow/blob/develop/test/couchtiny_example.rb[the full source code].
279
+
280
+
281
+ === Adapters to support other databases
282
+
283
+ I get a lot of requests to integrate persistence support for different
284
+ databases, object-relational adapters, column stores, document
285
+ databases.
286
+
287
+ To enable highest possible quality, avoid too many dependencies and to
288
+ avoid unneeded maintenance burden on the `workflow` core it is best to
289
+ implement such support as a separate gem.
290
+
291
+ Only support for the ActiveRecord will remain for the foreseeable
292
+ future. So Rails beginners can expect `workflow` to work with Rails out
293
+ of the box. Other already included adapters stay for a while but should
294
+ be extracted to separate gems.
295
+
296
+ If you want to implement support for your favorite ORM mapper or your
297
+ favorite NoSQL database, you just need to implement a module which
298
+ overrides the persistence methods `load_workflow_state` and
299
+ `persist_workflow_state`. Example:
300
+
301
+ ```rb
302
+ module Workflow
303
+ module SuperCoolDb
304
+ module InstanceMethods
305
+ def load_workflow_state
306
+ # Load and return the workflow_state from some storage.
307
+ # You can use self.class.workflow_column configuration.
308
+ end
309
+
310
+ def persist_workflow_state(new_value)
311
+ # save the new_value workflow state
312
+ end
313
+ end
314
+
315
+ module ClassMethods
316
+ # class methods of your adapter go here
317
+ end
318
+
319
+ def self.included(klass)
320
+ klass.send :include, InstanceMethods
321
+ klass.extend ClassMethods
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ The user of the adapter can use it then as:
328
+
329
+ ```rb
330
+ class Article
331
+ include Workflow
332
+ include Workflow:SuperCoolDb
333
+ workflow do
334
+ state :submitted
335
+ # ...
336
+ end
337
+ end
338
+ ```
339
+
340
+ I can then link to your implementation from this README. Please let me
341
+ also know, if you need any interface beyond `load_workflow_state` and
342
+ `persist_workflow_state` methods to implement an adapter for your
343
+ favorite database.
344
+
345
+ Advanced usage
346
+ --------------
347
+
348
+ ### Accessing your workflow specification
349
+
350
+ You can easily reflect on workflow specification programmatically - for
351
+ the whole class or for the current object. Examples:
352
+
353
+ ```rb
354
+ article2.current_state.events # lists possible events from here
355
+ article2.current_state.events[:reject].transitions_to # => :rejected
356
+
357
+ Article.workflow_spec.states.keys
358
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
359
+
360
+ Article.workflow_spec.state_names
361
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
362
+
363
+ # list all events for all states
364
+ Article.workflow_spec.states.values.collect &:events
365
+ ```
366
+
367
+ You can also store and later retrieve additional meta data for every
368
+ state and every event:
369
+
370
+ ```rb
371
+ class MyProcess
372
+ include Workflow
373
+ workflow do
374
+ state :main, :meta => {:importance => 8}
375
+ state :supplemental, :meta => {:importance => 1}
376
+ end
377
+ end
378
+ puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
379
+ ```
380
+
381
+ The workflow library itself uses this feature to tweak the graphical
382
+ representation of the workflow. See below.
383
+
384
+
385
+ ### Conditional event transitions
386
+
387
+ Conditions can be a "method name symbol" with a corresponding instance method, a `proc` or `lambda` which are added to events, like so:
388
+
389
+ ```rb
390
+ state :off
391
+ event :turn_on, :transition_to => :on,
392
+ :if => :sufficient_battery_level?
393
+
394
+ event :turn_on, :transition_to => :low_battery,
395
+ :if => proc { |device| device.battery_level > 0 }
396
+ end
397
+
398
+ # corresponding instance method
399
+ def sufficient_battery_level?
400
+ battery_level > 10
401
+ end
402
+ ```
403
+
404
+ When calling a `device.can_<fire_event>?` check, or attempting a `device.<event>!`, each event is checked in turn:
405
+
406
+ * With no `:if` check, proceed as usual.
407
+ * If an `:if` check is present, proceed if it evaluates to true, or drop to the next event.
408
+ * If you've run out of events to check (eg. `battery_level == 0`), then the transition isn't possible.
409
+
410
+
411
+ ### Advanced transition hooks
412
+
413
+ #### on_entry/on_exit
414
+
415
+ We already had a look at the declaring callbacks for particular workflow
416
+ events. If you would like to react to all transitions to/from the same state
417
+ in the same way you can use the on_entry/on_exit hooks. You can either define it
418
+ with a block inside the workflow definition or through naming
419
+ convention, e.g. for the state :pending just define the method
420
+ `on_pending_exit(new_state, event, *args)` somewhere in your class.
421
+
422
+ #### on_transition
423
+
424
+ If you want to be informed about everything happening everywhere, e.g. for
425
+ logging then you can use the universal `on_transition` hook:
426
+
427
+ ```rb
428
+ workflow do
429
+ state :one do
430
+ event :increment, :transitions_to => :two
431
+ end
432
+ state :two
433
+ on_transition do |from, to, triggering_event, *event_args|
434
+ Log.info "#{from} -> #{to}"
435
+ end
436
+ end
437
+ ```
438
+
439
+ #### on_error
440
+
441
+ If you want to do custom exception handling internal to workflow, you can define an `on_error` hook in your workflow.
442
+ For example:
443
+
444
+ ```rb
445
+ workflow do
446
+ state :first do
447
+ event :forward, :transitions_to => :second
448
+ end
449
+ state :second
450
+
451
+ on_error do |error, from, to, event, *args|
452
+ Log.info "Exception(#{error.class}) on #{from} -> #{to}"
453
+ end
454
+ end
455
+ ```
456
+
457
+ If forward! results in an exception, `on_error` is invoked and the workflow stays in a 'first' state. This capability
458
+ is particularly useful if your errors are transient and you want to queue up a job to retry in the future without
459
+ affecting the existing workflow state.
460
+
461
+ ### Guards
462
+
463
+ If you want to halt the transition conditionally, you can just raise an
464
+ exception in your [transition event handler](#transition_event_handler).
465
+ There is a helper called `halt!`, which raises the
466
+ Workflow::TransitionHalted exception. You can provide an additional
467
+ `halted_because` parameter.
468
+
469
+ ```rb
470
+ def reject(reason)
471
+ halt! 'We do not reject articles unless the reason is important' \
472
+ unless reason =~ /important/i
473
+ end
474
+ ```
475
+
476
+ The traditional `halt` (without the exclamation mark) is still supported
477
+ too. This just prevents the state change without raising an
478
+ exception.
479
+
480
+ You can check `halted?` and `halted_because` values later.
481
+
482
+ ### Hook order
483
+
484
+ The whole event sequence is as follows:
485
+
486
+ * before_transition
487
+ * event specific action
488
+ * on_transition (if action did not halt)
489
+ * on_exit
490
+ * PERSIST WORKFLOW STATE, i.e. transition
491
+ * on_entry
492
+ * after_transition
493
+
494
+
495
+ Documenting with diagrams
496
+ -------------------------
497
+
498
+ You can generate a graphical representation of the workflow for
499
+ a particular class for documentation purposes.
500
+ Use `Workflow::create_workflow_diagram(class)` in your rake task like:
501
+
502
+ ```rb
503
+ namespace :doc do
504
+ desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
505
+ task :workflow => :environment do
506
+ require 'workflow/draw'
507
+ Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
508
+ end
509
+ end
510
+ ```
511
+
512
+
513
+ Changelog
514
+ ---------
515
+
516
+ === New in the version 3.0.0
517
+
518
+ * gh-228 Support for Ruby 3 keyword args, provided by @agirling
519
+ * retire Ruby 2.6 since it has reached end of live; please use workflow 2.x, if you still depend on that Ruby version
520
+ * gh-229 Switch from travis CI to GihHub actions for continuous integration
521
+
522
+ ### New in the versions 2.x
523
+
524
+ * extract persistence adapters, Rails/ActiveRecord integration is now a separate gem
525
+ workflow-activerecord
526
+
527
+ Support, Participation
528
+ ----------------------
529
+
530
+ ### Reporting bugs
531
+
532
+ <http://github.com/geekq/workflow/issues>
533
+
534
+ ### Development Setup
535
+
536
+ ```sh
537
+ sudo apt-get install graphviz # Linux
538
+ brew install graphviz # Mac OS
539
+ cd workflow
540
+ gem install bundler
541
+ bundle install
542
+ # run all the tests
543
+ bundle exec rake test
544
+ ```
545
+
546
+ ### Other 3rd party libraries
547
+
548
+ https://github.com/kwent/active_admin-workflow[ActiveAdmin-Workflow] - is an
549
+ integration with https://github.com/activeadmin/activeadmin[ActiveAdmin].
550
+
551
+ ### About
552
+
553
+ Author: Vladimir Dobriakov, <https://infrastructure-as-code.de>
554
+
555
+ Copyright (c) 2010-2022 Vladimir Dobriakov and Contributors
556
+
557
+ Copyright (c) 2008-2009 Vodafone
558
+
559
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
560
+
561
+ Based on the work of Ryan Allen and Scott Barron
562
+
563
+ Licensed under MIT license, see the MIT-LICENSE file.
564
+
@@ -1,3 +1,3 @@
1
1
  module Workflow
2
- VERSION = "2.0.2"
2
+ VERSION = "3.0.0"
3
3
  end