workflow 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown DELETED
@@ -1,664 +0,0 @@
1
- [![Version ](https://badge.fury.io/rb/workflow.svg)](https://badge.fury.io/rb/workflow)
2
- [![Build Status ](https://travis-ci.org/geekq/workflow.svg)](https://travis-ci.org/geekq/workflow)
3
- [![Code Climate ](https://codeclimate.com/github/geekq/workflow/badges/gpa.svg)](https://codeclimate.com/github/geekq/workflow)
4
- [![Test Coverage](https://codeclimate.com/github/geekq/workflow/badges/coverage.svg)](https://codeclimate.com/github/geekq/workflow/coverage)
5
-
6
- # Workflow
7
-
8
- Note: you can find documentation for specific workflow rubygem versions
9
- at http://rubygems.org/gems/workflow : select a version (optional,
10
- default is latest release), click "Documentation" link. When reading on
11
- github.com, the README refers to the upcoming release.
12
-
13
- **Note: Workflow 2.0 is a major refactoring of the library.
14
- For different options/troubleshooting using it with your Rails application see
15
- [State persistence with ActiveRecord](#state-persistence-with-activerecord).**
16
-
17
- Note for contributors: it looks like github closed all the pull requests after
18
- I had changed the default branch on 2019-01-12. Please check the new refactored
19
- workflow 2.0, complementing workflow-activerecord and recreate your pull
20
- request if needed.
21
-
22
- What is workflow?
23
- -----------------
24
-
25
- Workflow is a finite-state-machine-inspired API for modeling and
26
- interacting with what we tend to refer to as 'workflow'.
27
-
28
- A lot of business modeling tends to involve workflow-like concepts, and
29
- the aim of this library is to make the expression of these concepts as
30
- clear as possible, using similar terminology as found in state machine
31
- theory.
32
-
33
- So, a workflow has a state. It can only be in one state at a time. When
34
- a workflow changes state, we call that a transition. Transitions occur
35
- on an event, so events cause transitions to occur. Additionally, when an
36
- event fires, other arbitrary code can be executed, we call those actions.
37
- So any given state has a bunch of events, any event in a state causes a
38
- transition to another state and potentially causes code to be executed
39
- (an action). We can hook into states when they are entered, and exited
40
- from, and we can cause transitions to fail (guards), and we can hook in
41
- to every transition that occurs ever for whatever reason we can come up
42
- with.
43
-
44
- Now, all that's a mouthful, but we'll demonstrate the API bit by bit
45
- with a real-ish world example.
46
-
47
- Let's say we're modeling article submission from journalists. An article
48
- is written, then submitted. When it's submitted, it's awaiting review.
49
- Someone reviews the article, and then either accepts or rejects it.
50
- Here is the expression of this workflow using the API:
51
-
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
- Nice, isn't it!
71
-
72
- Note: the first state in the definition (`:new` in the example, but you
73
- can name it as you wish) is used as the initial state - newly created
74
- objects start their life cycle in that state.
75
-
76
- Let's create an article instance and check in which state it is:
77
-
78
- article = Article.new
79
- article.accepted? # => false
80
- article.new? # => true
81
-
82
- You can also access the whole `current_state` object including the list
83
- of possible events and other meta information:
84
-
85
- article.current_state
86
- => #<Workflow::State:0x7f1e3d6731f0 @events={
87
- :submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
88
- @transitions_to=:awaiting_review, @name=:submit, @meta={}>},
89
- name:new, meta{}
90
-
91
- You can also check, whether a state comes before or after another state (by the
92
- order they were defined):
93
-
94
- article.current_state
95
- => being_reviewed
96
- article.current_state < :accepted
97
- => true
98
- article.current_state >= :accepted
99
- => false
100
- article.current_state.between? :awaiting_review, :rejected
101
- => true
102
-
103
- Now we can call the submit event, which transitions to the
104
- <tt>:awaiting_review</tt> state:
105
-
106
- article.submit!
107
- article.awaiting_review? # => true
108
-
109
- Events are actually instance methods on a workflow, and depending on the
110
- state you're in, you'll have a different set of events used to
111
- transition to other states.
112
-
113
- It is also easy to check, if a certain transition is possible from the
114
- current state . `article.can_submit?` checks if there is a `:submit`
115
- event (transition) defined for the current state.
116
-
117
-
118
- Installation
119
- ------------
120
-
121
- gem install workflow
122
-
123
- **Important**: If you're interested in graphing your workflow state machine, you will also need to
124
- install the `activesupport` and `ruby-graphviz` gems.
125
-
126
- Versions up to and including 1.0.0 are also available as a single file download -
127
- [lib/workflow.rb file](https://github.com/geekq/workflow/blob/v1.0.0/lib/workflow.rb).
128
-
129
-
130
- Examples
131
- --------
132
-
133
- After installation or downloading of the library you can easily try out
134
- all the example code from this README in irb.
135
-
136
- $ irb
137
- require 'rubygems'
138
- require 'workflow'
139
-
140
- Now just copy and paste the source code from the beginning of this README
141
- file snippet by snippet and observe the output.
142
-
143
-
144
- Transition event handler
145
- ------------------------
146
-
147
- The best way is to use convention over configuration and to define a
148
- method with the same name as the event. Then it is automatically invoked
149
- when event is raised. For the Article workflow defined earlier it would
150
- be:
151
-
152
- class Article
153
- def reject
154
- puts 'sending email to the author explaining the reason...'
155
- end
156
- end
157
-
158
- `article.review!; article.reject!` will cause state transition to
159
- `being_reviewed` state, persist the new state (if integrated with
160
- ActiveRecord), invoke this user defined `reject` method and finally
161
- persist the `rejected` state.
162
-
163
- Note: on successful transition from one state to another the workflow
164
- gem immediately persists the new workflow state with `update_column()`,
165
- bypassing any ActiveRecord callbacks including `updated_at` update.
166
- This way it is possible to deal with the validation and to save the
167
- pending changes to a record at some later point instead of the moment
168
- when transition occurs.
169
-
170
- You can also define event handler accepting/requiring additional
171
- arguments:
172
-
173
- class Article
174
- def review(reviewer = '')
175
- puts "[#{reviewer}] is now reviewing the article"
176
- end
177
- end
178
-
179
- article2 = Article.new
180
- article2.submit!
181
- article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
182
-
183
-
184
- ### The old, deprecated way
185
-
186
- The old way, using a block is still supported but deprecated:
187
-
188
- event :review, :transitions_to => :being_reviewed do |reviewer|
189
- # store the reviewer
190
- end
191
-
192
- We've noticed, that mixing the list of events and states with the blocks
193
- invoked for particular transitions leads to a bumpy and poorly readable code
194
- due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
195
- we decided to invoke an optional user defined callback method with the same
196
- name as the event (convention over configuration) as explained before.
197
-
198
- State persistence with ActiveRecord
199
- -----------------------------------
200
-
201
- Note: Workflow 2.0 is a major refactoring for the `worklow` library.
202
- If your application suddenly breaks after the workflow 2.0 release, you've
203
- probably got your Gemfile wrong ;-). workflow uses [semantic versioning][]. For
204
- highest compatibility please reference the desired major+minor version.
205
-
206
- Note on ActiveRecord/Rails 4.\*, 5.\* Support:
207
-
208
- Since integration with ActiveRecord makes over 90% of the issues and
209
- maintenance effort, and also to allow for an independent (faster) release cycle
210
- for Rails support, starting with workflow **version 2.0** in January 2019 the
211
- support for ActiveRecord (4.\*, 5.\* and newer) has been extracted into a separate
212
- gem. Read at [workflow-activerecord][], how to include the right gem.
213
-
214
- To use legacy built-in ActiveRecord 2.3 - 4.* support, reference Workflow 1.2 in
215
- your Gemfile:
216
-
217
- gem 'workflow', '~> 1.2'
218
-
219
- [semantic versioning]: https://guides.rubygems.org/patterns/#semantic-versioning
220
- [workflow-activerecord]: https://github.com/geekq/workflow-activerecord
221
-
222
- Custom workflow state persistence
223
- ---------------------------------
224
-
225
- If you do not use a relational database and ActiveRecord, you can still
226
- integrate the workflow very easily. To implement persistence you just
227
- need to override `load_workflow_state` and
228
- `persist_workflow_state(new_value)` methods. Next section contains an example for
229
- using CouchDB, a document oriented database.
230
-
231
- [Tim Lossen](http://tim.lossen.de/) implemented support
232
- for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
233
- key-value store.
234
-
235
- Integration with CouchDB
236
- ------------------------
237
-
238
- We are using the compact [couchtiny library](http://github.com/geekq/couchtiny)
239
- here. But the implementation would look similar for the popular
240
- couchrest library.
241
-
242
- require 'couchtiny'
243
- require 'couchtiny/document'
244
- require 'workflow'
245
-
246
- class User < CouchTiny::Document
247
- include Workflow
248
- workflow do
249
- state :submitted do
250
- event :activate_via_link, :transitions_to => :proved_email
251
- end
252
- state :proved_email
253
- end
254
-
255
- def load_workflow_state
256
- self[:workflow_state]
257
- end
258
-
259
- def persist_workflow_state(new_value)
260
- self[:workflow_state] = new_value
261
- save!
262
- end
263
- end
264
-
265
- Please also have a look at
266
- [the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
267
-
268
-
269
- Adapters to support other databases
270
- -----------------------------------
271
-
272
- I get a lot of requests to integrate persistence support for different
273
- databases, object-relational adapters, column stores, document
274
- databases.
275
-
276
- To enable highest possible quality, avoid too many dependencies and to
277
- avoid unneeded maintenance burden on the `workflow` core it is best to
278
- implement such support as a separate gem.
279
-
280
- Only support for the ActiveRecord will remain for the foreseeable
281
- future. So Rails beginners can expect `workflow` to work with Rails out
282
- of the box. Other already included adapters stay for a while but should
283
- be extracted to separate gems.
284
-
285
- If you want to implement support for your favorite ORM mapper or your
286
- favorite NoSQL database, you just need to implement a module which
287
- overrides the persistence methods `load_workflow_state` and
288
- `persist_workflow_state`. Example:
289
-
290
- module Workflow
291
- module SuperCoolDb
292
- module InstanceMethods
293
- def load_workflow_state
294
- # Load and return the workflow_state from some storage.
295
- # You can use self.class.workflow_column configuration.
296
- end
297
-
298
- def persist_workflow_state(new_value)
299
- # save the new_value workflow state
300
- end
301
- end
302
-
303
- module ClassMethods
304
- # class methods of your adapter go here
305
- end
306
-
307
- def self.included(klass)
308
- klass.send :include, InstanceMethods
309
- klass.extend ClassMethods
310
- end
311
- end
312
- end
313
-
314
- The user of the adapter can use it then as:
315
-
316
- class Article
317
- include Workflow
318
- include Workflow:SuperCoolDb
319
- workflow do
320
- state :submitted
321
- # ...
322
- end
323
- end
324
-
325
- I can then link to your implementation from this README. Please let me
326
- also know, if you need any interface beyond `load_workflow_state` and
327
- `persist_workflow_state` methods to implement an adapter for your
328
- favorite database.
329
-
330
-
331
- Accessing your workflow specification
332
- -------------------------------------
333
-
334
- You can easily reflect on workflow specification programmatically - for
335
- the whole class or for the current object. Examples:
336
-
337
- article2.current_state.events # lists possible events from here
338
- article2.current_state.events[:reject].transitions_to # => :rejected
339
-
340
- Article.workflow_spec.states.keys
341
- #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
342
-
343
- Article.workflow_spec.state_names
344
- #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
345
-
346
- # list all events for all states
347
- Article.workflow_spec.states.values.collect &:events
348
-
349
-
350
- You can also store and later retrieve additional meta data for every
351
- state and every event:
352
-
353
- class MyProcess
354
- include Workflow
355
- workflow do
356
- state :main, :meta => {:importance => 8}
357
- state :supplemental, :meta => {:importance => 1}
358
- end
359
- end
360
- puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
361
-
362
- The workflow library itself uses this feature to tweak the graphical
363
- representation of the workflow. See below.
364
-
365
-
366
- Conditional event transitions
367
- -----------------------------
368
-
369
- Conditions can be a "method name symbol" with a corresponding instance method, a `proc` or `lambda` which are added to events, like so:
370
-
371
- state :off
372
- event :turn_on, :transition_to => :on,
373
- :if => :sufficient_battery_level?
374
-
375
- event :turn_on, :transition_to => :low_battery,
376
- :if => proc { |device| device.battery_level > 0 }
377
- end
378
-
379
- # corresponding instance method
380
- def sufficient_battery_level?
381
- battery_level > 10
382
- end
383
-
384
- When calling a `device.can_<fire_event>?` check, or attempting a `device.<event>!`, each event is checked in turn:
385
-
386
- * With no `:if` check, proceed as usual.
387
- * If an `:if` check is present, proceed if it evaluates to true, or drop to the next event.
388
- * If you've run out of events to check (eg. `battery_level == 0`), then the transition isn't possible.
389
-
390
-
391
- Advanced transition hooks
392
- -------------------------
393
-
394
- ### on_entry/on_exit
395
-
396
- We already had a look at the declaring callbacks for particular workflow
397
- events. If you would like to react to all transitions to/from the same state
398
- in the same way you can use the on_entry/on_exit hooks. You can either define it
399
- with a block inside the workflow definition or through naming
400
- convention, e.g. for the state :pending just define the method
401
- `on_pending_exit(new_state, event, *args)` somewhere in your class.
402
-
403
- ### on_transition
404
-
405
- If you want to be informed about everything happening everywhere, e.g. for
406
- logging then you can use the universal `on_transition` hook:
407
-
408
- workflow do
409
- state :one do
410
- event :increment, :transitions_to => :two
411
- end
412
- state :two
413
- on_transition do |from, to, triggering_event, *event_args|
414
- Log.info "#{from} -> #{to}"
415
- end
416
- end
417
-
418
- ### on_error
419
-
420
- If you want to do custom exception handling internal to workflow, you can define an `on_error` hook in your workflow.
421
- For example:
422
-
423
- workflow do
424
- state :first do
425
- event :forward, :transitions_to => :second
426
- end
427
- state :second
428
-
429
- on_error do |error, from, to, event, *args|
430
- Log.info "Exception(#error.class) on #{from} -> #{to}"
431
- end
432
- end
433
-
434
- If forward! results in an exception, `on_error` is invoked and the workflow stays in a 'first' state. This capability
435
- is particularly useful if your errors are transient and you want to queue up a job to retry in the future without
436
- affecting the existing workflow state.
437
-
438
- ### Guards
439
-
440
- If you want to halt the transition conditionally, you can just raise an
441
- exception in your [transition event handler](#transition_event_handler).
442
- There is a helper called `halt!`, which raises the
443
- Workflow::TransitionHalted exception. You can provide an additional
444
- `halted_because` parameter.
445
-
446
- def reject(reason)
447
- halt! 'We do not reject articles unless the reason is important' \
448
- unless reason =~ /important/i
449
- end
450
-
451
- The traditional `halt` (without the exclamation mark) is still supported
452
- too. This just prevents the state change without raising an
453
- exception.
454
-
455
- You can check `halted?` and `halted_because` values later.
456
-
457
- ### Hook order
458
-
459
- The whole event sequence is as follows:
460
-
461
- * before_transition
462
- * event specific action
463
- * on_transition (if action did not halt)
464
- * on_exit
465
- * PERSIST WORKFLOW STATE, i.e. transition
466
- * on_entry
467
- * after_transition
468
-
469
-
470
- Documenting with diagrams
471
- -------------------------
472
-
473
- You can generate a graphical representation of the workflow for
474
- a particular class for documentation purposes.
475
- Use `Workflow::create_workflow_diagram(class)` in your rake task like:
476
-
477
- namespace :doc do
478
- desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
479
- task :workflow => :environment do
480
- require 'workflow/draw'
481
- Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
482
- end
483
- end
484
-
485
-
486
- Development Setup
487
- -----------------
488
-
489
- sudo apt-get install graphviz # Linux
490
- brew cask install graphviz # Mac OS
491
- cd workflow
492
- gem install bundler
493
- bundle install
494
- # run all the tests
495
- bundle exec rake test
496
-
497
-
498
- Changelog
499
- ---------
500
-
501
- ### New in the version 2.0.2
502
-
503
- * finalize extraction of persistence adapters, remove remodel adapter
504
-
505
- ### New in the version 2.0.1
506
-
507
- * retire Ruby 2.3 since it has reached end of live
508
- * fix #213 ruby-graphiz warnings
509
-
510
- ### New in the version 2.0.0
511
-
512
- * extract Rails/ActiveRecord integration into a separate gem
513
- workflow-activerecord
514
- * Remodel integration removed - needs to be a separate gem
515
-
516
- Special thanks to [voltechs][] for implementing Rails 5 support
517
- and helping to revive `workflow`!
518
-
519
- [voltechs]: https://github.com/voltechs
520
-
521
- ### New in the upcoming version 1.3.0 (never released)
522
-
523
- * Retiring Ruby 1.8.7 and Rails 2 support #118. If you still need this older
524
- versions despite security issues and missing updates, you can use
525
- workflow 1.2.0 or older. In your Gemfile put
526
-
527
- gem 'workflow', '~> 1.2.0'
528
-
529
- or when using github source just reference the v1.2.0 tag.
530
- * improved callback method handling: #113 and #125
531
-
532
- ### New in the version 1.2.0
533
-
534
- * Fix issue #98 protected on\_\* callbacks in Ruby 2
535
- * #106 Inherit exceptions from StandardError instead of Exception
536
- * #109 Conditional event transitions, contributed by [damncabbage](http://robhoward.id.au/)
537
- Please note: this introduces incompatible changes to the meta data API, see also #131.
538
- * New policy for supporting other databases - extract to separate
539
- gems. See the README section above.
540
- * #111 Custom Versions of Existing Adapters by [damncabbage](http://robhoward.id.au/)
541
-
542
-
543
- ### New in the version 1.1.0
544
-
545
- * Tested with ActiveRecord 4.0 (Rails 4.0)
546
- * Tested with Ruby 2.0
547
- * automatically generated scopes with names based on state names
548
- * clean workflow definition override for class inheritance - undefining
549
- the old convinience methods, s. <http://git.io/FZO02A>
550
-
551
- ### New in the version 1.0.0
552
-
553
- * **Support to private/protected callback methods.**
554
- See also issues [#53](https://github.com/geekq/workflow/pull/53)
555
- and [#58](https://github.com/geekq/workflow/pull/58). With the new
556
- implementation:
557
-
558
- * callback methods can be hidden (non public): both private methods
559
- in the immediate class and protected methods somewhere in the class
560
- hierarchy are supported
561
- * no unintentional calls on `fail!` and other Kernel methods
562
- * inheritance hierarchy with workflow is supported
563
-
564
- * using Rails' 3.1 `update_column` whenever available so only the
565
- workflow state column and not other pending attribute changes are
566
- saved on state transition. Fallback to `update_attribute` for older
567
- Rails and other ORMs. [commit](https://github.com/geekq/workflow/commit/7e091d8ded1aeeb0a86647bbf7d78ab3c9d0c458)
568
-
569
- ### New in the version 0.8.7
570
-
571
- * switch from [jeweler][] to pure bundler for building gems
572
-
573
- ### New in the version 0.8.0
574
-
575
- * check if a certain transition possible from the current state with
576
- `can_....?`
577
- * fix workflow_state persistence for multiple_workflows example
578
- * add before_transition and after_transition hooks as suggested by
579
- [kasperbn](https://github.com/kasperbn)
580
-
581
- ### New in the version 0.7.0
582
-
583
- * fix issue#10 Workflow::create_workflow_diagram documentation and path
584
- escaping
585
- * fix issue#7 workflow_column does not work STI (single table
586
- inheritance) ActiveRecord models
587
- * fix issue#5 Diagram generation fails for models in modules
588
-
589
- ### New in the version 0.6.0
590
-
591
- * enable multiple workflows by connecting workflow to object instances
592
- (using metaclass) instead of connecting to a class, s. "Multiple
593
- Workflows" section
594
-
595
- ### New in the version 0.5.0
596
-
597
- * fix issue#3 change the behaviour of halt! to immediately raise an
598
- exception. See also http://github.com/geekq/workflow/issues/#issue/3
599
-
600
- ### New in the version 0.4.0
601
-
602
- * completely rewritten the documentation to match my branch
603
- * switch to [jeweler][] for building gems
604
- * use [gemcutter][] for gem distribution
605
- * every described feature is backed up by an automated test
606
-
607
- [jeweler]: http://github.com/technicalpickles/jeweler
608
- [gemcutter]: http://gemcutter.org/gems/workflow
609
-
610
- ### New in the version 0.3.0
611
-
612
- Intermixing of transition graph definition (states, transitions)
613
- on the one side and implementation of the actions on the other side
614
- for a bigger state machine can introduce clutter.
615
-
616
- To reduce this clutter it is now possible to use state entry- and
617
- exit- hooks defined through a naming convention. For example, if there
618
- is a state :pending, then instead of using a
619
- block:
620
-
621
- state :pending do
622
- on_entry do
623
- # your implementation here
624
- end
625
- end
626
-
627
- you can hook in by defining method
628
-
629
- def on_pending_exit(new_state, event, *args)
630
- # your implementation here
631
- end
632
-
633
- anywhere in your class. You can also use a simpler function signature
634
- like `def on_pending_exit(*args)` if your are not interested in
635
- arguments. Please note: `def on_pending_exit()` with an empty list
636
- would not work.
637
-
638
- If both a function with a name according to naming convention and the
639
- on_entry/on_exit block are given, then only on_entry/on_exit block is used.
640
-
641
-
642
- Support
643
- -------
644
-
645
- ### Reporting bugs
646
-
647
- <http://github.com/geekq/workflow/issues>
648
-
649
-
650
- About
651
- -----
652
-
653
- Author: Vladimir Dobriakov, <https://infrastructure-as-code.de>
654
-
655
- Copyright (c) 2010-2019 Vladimir Dobriakov and Contributors
656
-
657
- Copyright (c) 2008-2009 Vodafone
658
-
659
- Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
660
-
661
- Based on the work of Ryan Allen and Scott Barron
662
-
663
- Licensed under MIT license, see the MIT-LICENSE file.
664
-