workflow-orchestrator 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +36 -0
  4. data/CHANGELOG.md +133 -0
  5. data/Gemfile +3 -0
  6. data/MIT-LICENSE +22 -0
  7. data/README.md +707 -0
  8. data/Rakefile +30 -0
  9. data/gemfiles/Gemfile.rails-3.x +12 -0
  10. data/gemfiles/Gemfile.rails-4.0 +14 -0
  11. data/gemfiles/Gemfile.rails-4.1 +14 -0
  12. data/gemfiles/Gemfile.rails-4.2 +14 -0
  13. data/gemfiles/Gemfile.rails-edge +14 -0
  14. data/lib/workflow/adapters/active_record.rb +75 -0
  15. data/lib/workflow/adapters/remodel.rb +15 -0
  16. data/lib/workflow/draw.rb +79 -0
  17. data/lib/workflow/errors.rb +20 -0
  18. data/lib/workflow/event.rb +38 -0
  19. data/lib/workflow/event_collection.rb +36 -0
  20. data/lib/workflow/specification.rb +83 -0
  21. data/lib/workflow/state.rb +44 -0
  22. data/lib/workflow/version.rb +3 -0
  23. data/lib/workflow.rb +307 -0
  24. data/orders_workflow.png +0 -0
  25. data/test/active_record_scopes_test.rb +56 -0
  26. data/test/active_record_scopes_with_values_test.rb +79 -0
  27. data/test/adapter_hook_test.rb +52 -0
  28. data/test/advanced_examples_test.rb +84 -0
  29. data/test/advanced_hooks_and_validation_test.rb +119 -0
  30. data/test/attr_protected_test.rb +107 -0
  31. data/test/before_transition_test.rb +36 -0
  32. data/test/couchtiny_example.rb +46 -0
  33. data/test/enum_values_in_memory_test.rb +23 -0
  34. data/test/enum_values_test.rb +30 -0
  35. data/test/incline_column_test.rb +54 -0
  36. data/test/inheritance_test.rb +56 -0
  37. data/test/main_test.rb +588 -0
  38. data/test/multiple_workflows_test.rb +84 -0
  39. data/test/new_versions/compare_states_test.rb +32 -0
  40. data/test/new_versions/persistence_test.rb +62 -0
  41. data/test/on_error_test.rb +52 -0
  42. data/test/on_unavailable_transition_test.rb +85 -0
  43. data/test/readme_example.rb +37 -0
  44. data/test/test_helper.rb +39 -0
  45. data/test/without_active_record_test.rb +54 -0
  46. data/workflow-orchestrator.gemspec +42 -0
  47. metadata +267 -0
data/README.md ADDED
@@ -0,0 +1,707 @@
1
+ # Workflow Orchestrator [![Build Status](https://travis-ci.org/lorefnon/workflow-orchestrator.svg?branch=master)](https://travis-ci.org/lorefnon/workflow-orchestrator)
2
+
3
+ A ruby DSL for modeling business logic as [Finite State Machines](https://en.wikipedia.org/wiki/Finite-state_machine).
4
+
5
+ The aim of this library is to make the expression of these concepts as clear as possible, utilizing the expressiveness of ruby language, and using similar terminology as found in state machine theory.
6
+
7
+ ## Concepts
8
+
9
+ - **State:** A workflow is in exactly one state at a time. State may optionally be persisted using ActiveRecord.
10
+ - **State transition:** Change of state can be observed and intercepted
11
+ - **Events:** Events cause state transitions to occur
12
+ - **Actions:** Actions constitute of parts of our business logic which are executed in response to state transitions.
13
+
14
+ We can hook into states when they are entered, and exited from, and we can cause transitions to fail (guards), and we can hook in to every transition that occurs ever for whatever reason we can come up with.
15
+
16
+ ## Example
17
+
18
+ Let's say we're modeling article submission from journalists. An article
19
+ is written, then submitted. When it's submitted, it's awaiting review.
20
+ Someone reviews the article, and then either accepts or rejects it.
21
+ Here is the expression of this workflow using the API:
22
+
23
+ ```ruby
24
+ class Article
25
+ include Workflow
26
+ workflow do
27
+ state :new do
28
+ event :submit, :transitions_to => :awaiting_review
29
+ end
30
+ state :awaiting_review do
31
+ event :review, :transitions_to => :being_reviewed
32
+ end
33
+ state :being_reviewed do
34
+ event :accept, :transitions_to => :accepted
35
+ event :reject, :transitions_to => :rejected
36
+ end
37
+ state :accepted
38
+ state :rejected
39
+ end
40
+ end
41
+ ```
42
+
43
+ Nice, isn't it!
44
+
45
+ Note: the first state in the definition (`:new` in the example, but you
46
+ can name it as you wish) is used as the initial state - newly created
47
+ objects start their life cycle in that state.
48
+
49
+ Let's create an article instance and check in which state it is:
50
+
51
+ ```ruby
52
+ article = Article.new
53
+ article.accepted? # => false
54
+ article.new? # => true
55
+ ```
56
+
57
+ You can also access the whole `current_state` object including the list
58
+ of possible events and other meta information:
59
+
60
+ ```ruby
61
+ article.current_state
62
+ # => #<Workflow::State:0x007fa1ab36f750
63
+ # @events={:submit=>#<Workflow::Event:0x007fa1ab36f638 @action=nil, @meta={}, @name=:submit, @transitions_to=:awaiting_review>},
64
+ # @meta={},
65
+ # @name=:new
66
+ ```
67
+
68
+ On Ruby 1.9 and above, you can check whether a state comes before or
69
+ after another state (by the order they were defined):
70
+
71
+ ```ruby
72
+ article.current_state.name
73
+ # => being_reviewed
74
+ article.current_state < :accepted
75
+ # => true
76
+ article.current_state >= :accepted
77
+ # => false
78
+ article.current_state.between? :awaiting_review, :rejected
79
+ # => true
80
+ ```
81
+
82
+ Now we can call the submit event, which transitions to the
83
+ <tt>:awaiting_review</tt> state:
84
+
85
+ ```ruby
86
+ article.submit!
87
+ article.awaiting_review? # => true
88
+ ```
89
+
90
+ Events are actually instance methods on a workflow, and depending on the
91
+ state you're in, you'll have a different set of events used to
92
+ transition to other states.
93
+
94
+ It is also easy to check, if a certain transition is possible from the
95
+ current state. `article.can_submit?` checks if there is a `:submit`
96
+ event (transition) defined for the current state.
97
+
98
+
99
+ Installation
100
+ ------------
101
+
102
+ `gem install workflow`
103
+
104
+ `include Workflow` in your model.
105
+
106
+ If you're using ActiveRecord, Workflow will by default use a "workflow_state" column on your model.
107
+
108
+ **Important**: If you're interested in graphing your workflow state machine, you will also need to
109
+ install the `activesupport` and `ruby-graphviz` gems.
110
+
111
+ Transition event handler
112
+ ------------------------
113
+
114
+ The best way is to use convention over configuration and to define a
115
+ method with the same name as the event. Then it is automatically invoked
116
+ when event is raised. For the Article workflow defined earlier it would
117
+ be:
118
+
119
+ ```ruby
120
+ class Article
121
+ def reject
122
+ puts 'sending email to the author explaining the reason...'
123
+ end
124
+ end
125
+ ```
126
+
127
+ `article.review!; article.reject!` will cause state transition to
128
+ `being_reviewed` state, persist the new state (if integrated with
129
+ ActiveRecord), invoke this user defined `reject` method and finally
130
+ persist the `rejected` state.
131
+
132
+ Note: on successful transition from one state to another the workflow
133
+ gem immediately persists the new workflow state with `update_column()`,
134
+ bypassing any ActiveRecord callbacks including `updated_at` update.
135
+ This way it is possible to deal with the validation and to save the
136
+ pending changes to a record at some later point instead of the moment
137
+ when transition occurs.
138
+
139
+ You can also define event handler accepting/requiring additional
140
+ arguments:
141
+
142
+ ```ruby
143
+ class Article
144
+ def review(reviewer = '')
145
+ puts "[#{reviewer}] is now reviewing the article"
146
+ end
147
+ end
148
+
149
+ article2 = Article.new
150
+ article2.submit!
151
+ article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
152
+ ```
153
+
154
+ ### The old, deprecated way
155
+
156
+ The old way, using a block is still supported but deprecated:
157
+
158
+ ```ruby
159
+ event :review, :transitions_to => :being_reviewed do |reviewer|
160
+ # store the reviewer
161
+ end
162
+ ```
163
+
164
+ We've noticed, that mixing the list of events and states with the blocks
165
+ invoked for particular transitions leads to a bumpy and poorly readable code
166
+ due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
167
+ we decided to invoke an optional user defined callback method with the same
168
+ name as the event (convention over configuration) as explained before.
169
+
170
+
171
+ Integration with ActiveRecord
172
+ -----------------------------
173
+
174
+ Workflow library can handle the state persistence fully automatically. You
175
+ only need to define a string field on the table called `workflow_state`
176
+ and include the workflow mixin in your model class as usual:
177
+
178
+ ```ruby
179
+ class Order < ActiveRecord::Base
180
+ include Workflow
181
+ workflow do
182
+ # list states and transitions here
183
+ end
184
+ end
185
+ ```
186
+
187
+ On a database record loading all the state check methods e.g.
188
+ `article.state`, `article.awaiting_review?` are immediately available.
189
+ For new records or if the `workflow_state` field is not set the state
190
+ defaults to the first state declared in the workflow specification. In
191
+ our example it is `:new`, so `Article.new.new?` returns true and
192
+ `Article.new.approved?` returns false.
193
+
194
+ At the end of a successful state transition like `article.approve!` the
195
+ new state is immediately saved in the database.
196
+
197
+ You can change this behaviour by overriding `persist_workflow_state`
198
+ method.
199
+
200
+ ### Scopes
201
+
202
+ Workflow library also adds automatically generated scopes with names based on
203
+ states names:
204
+
205
+ ```ruby
206
+ class Order < ActiveRecord::Base
207
+ include Workflow
208
+ workflow do
209
+ state :approved
210
+ state :pending
211
+ end
212
+ end
213
+
214
+ # returns all orders with `approved` state
215
+ Order.with_approved_state
216
+
217
+ # returns all orders except for those having `approved` state
218
+ Order.without_approved_state
219
+
220
+ # returns all orders except for those having `pending` state
221
+ Order.without_pending_state
222
+ ```
223
+
224
+
225
+ ### Custom workflow database column
226
+
227
+ [meuble](http://imeuble.info/) contributed a solution for using
228
+ custom persistence column easily, e.g. for a legacy database schema:
229
+
230
+ ```ruby
231
+ class LegacyOrder < ActiveRecord::Base
232
+ include Workflow
233
+
234
+ workflow_column :foo_bar # use this legacy database column for
235
+ # persistence
236
+ end
237
+ ```
238
+
239
+ You can also set the column name inline into the workflow block:
240
+
241
+ ```ruby
242
+ class LegacyOrder < ActiveRecord::Base
243
+ include Workflow
244
+
245
+ workflow :foo_bar do
246
+ state :approved
247
+ state :pending
248
+ end
249
+ end
250
+ ```
251
+
252
+ ### Single table inheritance
253
+
254
+ Single table inheritance is also supported. Descendant classes can either
255
+ inherit the workflow definition from the parent or override with its own
256
+ definition.
257
+
258
+ Custom workflow state persistence
259
+ ---------------------------------
260
+
261
+ If you do not use a relational database and ActiveRecord, you can still
262
+ integrate the workflow very easily. To implement persistence you just
263
+ need to override `load_workflow_state` and
264
+ `persist_workflow_state(new_value)` methods. Next section contains an example for
265
+ using CouchDB, a document oriented database.
266
+
267
+ [Tim Lossen](http://tim.lossen.de/) implemented support
268
+ for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
269
+ key-value store.
270
+
271
+ Integration with CouchDB
272
+ ------------------------
273
+
274
+ We are using the compact [couchtiny library](http://github.com/geekq/couchtiny)
275
+ here. But the implementation would look similar for the popular
276
+ couchrest library.
277
+
278
+ ```ruby
279
+ require 'couchtiny'
280
+ require 'couchtiny/document'
281
+ require 'workflow'
282
+
283
+ class User < CouchTiny::Document
284
+ include Workflow
285
+ workflow do
286
+ state :submitted do
287
+ event :activate_via_link, :transitions_to => :proved_email
288
+ end
289
+ state :proved_email
290
+ end
291
+
292
+ def load_workflow_state
293
+ self[:workflow_state]
294
+ end
295
+
296
+ def persist_workflow_state(new_value)
297
+ self[:workflow_state] = new_value
298
+ save!
299
+ end
300
+ end
301
+ ```
302
+
303
+ Please also have a look at
304
+ [the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
305
+
306
+
307
+ Adapters to support other databases
308
+ -----------------------------------
309
+
310
+ I get a lot of requests to integrate persistence support for different
311
+ databases, object-relational adapters, column stores, document
312
+ databases.
313
+
314
+ To enable highest possible quality, avoid too many dependencies and to
315
+ avoid unneeded maintenance burden on the `workflow` core it is best to
316
+ implement such support as a separate gem.
317
+
318
+ Only support for the ActiveRecord will remain for the foreseeable
319
+ future. So Rails beginners can expect `workflow` to work with Rails out
320
+ of the box. Other already included adapters stay for a while but should
321
+ be extracted to separate gems.
322
+
323
+ If you want to implement support for your favorite ORM mapper or your
324
+ favorite NoSQL database, you just need to implement a module which
325
+ overrides the persistence methods `load_workflow_state` and
326
+ `persist_workflow_state`. Example:
327
+
328
+ ```ruby
329
+ module Workflow
330
+ module SuperCoolDb
331
+ module InstanceMethods
332
+ def load_workflow_state
333
+ # Load and return the workflow_state from some storage.
334
+ # You can use self.class.workflow_column configuration.
335
+ end
336
+
337
+ def persist_workflow_state(new_value)
338
+ # save the new_value workflow state
339
+ end
340
+ end
341
+
342
+ module ClassMethods
343
+ # class methods of your adapter go here
344
+ end
345
+
346
+ def self.included(klass)
347
+ klass.send :include, InstanceMethods
348
+ klass.extend ClassMethods
349
+ end
350
+ end
351
+ end
352
+ ```
353
+
354
+ The user of the adapter can use it then as:
355
+
356
+ ```ruby
357
+ class Article
358
+ include Workflow
359
+ include Workflow::SuperCoolDb
360
+ workflow do
361
+ state :submitted
362
+ # ...
363
+ end
364
+ end
365
+ ```
366
+
367
+ I can then link to your implementation from this README. Please let me
368
+ also know, if you need any interface beyond `load_workflow_state` and
369
+ `persist_workflow_state` methods to implement an adapter for your
370
+ favorite database.
371
+
372
+
373
+ Custom Versions of Existing Adapters
374
+ ------------------------------------
375
+
376
+ Other adapters (such as a custom ActiveRecord plugin) can be selected by adding a `workflow_adapter` class method, eg.
377
+
378
+ ```ruby
379
+ class Example < ActiveRecord::Base
380
+ def self.workflow_adapter
381
+ MyCustomAdapter
382
+ end
383
+ include Workflow
384
+
385
+ # ...
386
+ end
387
+ ```
388
+
389
+ (The above will include `MyCustomAdapter` *instead* of `Workflow::Adapter::ActiveRecord`.)
390
+
391
+
392
+ Accessing your workflow specification
393
+ -------------------------------------
394
+
395
+ You can easily reflect on workflow specification programmatically - for
396
+ the whole class or for the current object. Examples:
397
+
398
+ ```ruby
399
+ article2.current_state.events # lists possible events from here
400
+ article2.current_state.events[:reject].transitions_to # => :rejected
401
+
402
+ Article.workflow_spec.states.keys
403
+ # => [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
404
+
405
+ Article.workflow_spec.state_names
406
+ # => [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
407
+
408
+ # list all events for all states
409
+ Article.workflow_spec.states.values.collect &:events
410
+ ```
411
+
412
+ You can also store and later retrieve additional meta data for every
413
+ state and every event:
414
+
415
+ ```ruby
416
+ class MyProcess
417
+ include Workflow
418
+ workflow do
419
+ state :main, :meta => {:importance => 8}
420
+ state :supplemental, :meta => {:importance => 1}
421
+ end
422
+ end
423
+ puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
424
+ ```
425
+
426
+ The workflow library itself uses this feature to tweak the graphical
427
+ representation of the workflow. See below.
428
+
429
+
430
+ Conditional event transitions
431
+ -----------------------------
432
+
433
+ Conditions can be a "method name symbol" with a corresponding instance method, a `proc` or `lambda` which are added to events, like so:
434
+
435
+ ```ruby
436
+ state :off do
437
+ event :turn_on, :transition_to => :on,
438
+ :if => :sufficient_battery_level?
439
+
440
+ event :turn_on, :transition_to => :low_battery,
441
+ :if => proc { |device| device.battery_level > 0 }
442
+ end
443
+
444
+ # corresponding instance method
445
+ def sufficient_battery_level?
446
+ battery_level > 10
447
+ end
448
+ ```
449
+
450
+ When calling a `device.can_<fire_event>?` check, or attempting a `device.<event>!`, each event is checked in turn:
451
+
452
+ * With no `:if` check, proceed as usual.
453
+ * If an `:if` check is present, proceed if it evaluates to true, or drop to the next event.
454
+ * If you've run out of events to check (eg. `battery_level == 0`), then the transition isn't possible.
455
+
456
+ Enum values or other custom values
457
+ -----------------------------------
458
+
459
+ If you don't want to store your state as a string column, you can specify the value associated with each state. Yu can use an int (like an enum) or a shorter string, or whatever you want.
460
+
461
+ Just pass the "value" for the state as the second parameter to the "state" method.
462
+
463
+ Class Foo < ActiveRecord::Base
464
+ include Workflow
465
+
466
+ workflow do
467
+ state :one, 1 do
468
+ event :increment, :transitions_to => :two
469
+ end
470
+ state :two, 2
471
+ on_transition do |from, to, triggering_event, *event_args|
472
+ Log.info "#{from} -> #{to}"
473
+ end
474
+ end
475
+ end
476
+
477
+ Your database column will store the values 1, 2, etc. But you'll still use the state symbols for querying.
478
+
479
+ foo = Foo.create
480
+ foo.current_state # => :one
481
+ foo.workflow_state # => 1 #You really shouldn't use this column directly...
482
+ foo.increment!
483
+ foo.two? # => true
484
+ foo.workflow_state # => true
485
+
486
+ Hopefully obvious, but if you ever change the value of a state, you'll need to do a migration/address existing records in your data store. However you are free to change the "name" of a state, willy-nilly.
487
+
488
+ Advanced transition hooks
489
+ -------------------------
490
+
491
+ ### `on_entry`/`on_exit`
492
+
493
+ We already had a look at the declaring callbacks for particular workflow
494
+ events. If you would like to react to all transitions to/from the same state
495
+ in the same way you can use the `on_entry`/`on_exit` hooks. You can either define it
496
+ with a block inside the workflow definition or through naming
497
+ convention, e.g. for the state :pending just define the method
498
+ `on_pending_exit(new_state, event, *args)` somewhere in your class.
499
+
500
+ ### `on_transition`
501
+
502
+ If you want to be informed about everything happening everywhere, e.g. for
503
+ logging then you can use the universal `on_transition` hook:
504
+
505
+ ```ruby
506
+ workflow do
507
+ state :one do
508
+ event :increment, :transitions_to => :two
509
+ end
510
+ state :two
511
+ on_transition do |from, to, triggering_event, *event_args|
512
+ Log.info "#{from} -> #{to}"
513
+ end
514
+ end
515
+ ```
516
+
517
+ Please also have a look at the [advanced end to end
518
+ example][advanced_hooks_and_validation_test].
519
+
520
+ [advanced_hooks_and_validation_test]: http://github.com/geekq/workflow/blob/master/test/advanced_hooks_and_validation_test.rb
521
+
522
+ ### `on_error`
523
+
524
+ If you want to do custom exception handling internal to workflow, you can define an `on_error` hook in your workflow.
525
+ For example:
526
+
527
+ ```ruby
528
+ workflow do
529
+ state :first do
530
+ event :forward, :transitions_to => :second
531
+ end
532
+ state :second
533
+
534
+ on_error do |error, from, to, event, *args|
535
+ Log.info "Exception(#error.class) on #{from} -> #{to}"
536
+ end
537
+ end
538
+ ```
539
+
540
+ If forward! results in an exception, `on_error` is invoked and the workflow stays in a 'first' state. This capability
541
+ is particularly useful if your errors are transient and you want to queue up a job to retry in the future without
542
+ affecting the existing workflow state.
543
+
544
+ Note: this is not triggered by Workflow::NoTransitionAllowed exceptions.
545
+
546
+ ### on_unavailable_transition
547
+
548
+ If you want to do custom handling when an unavailable transition is called, you can define an 'on_unavailable_transition' hook
549
+ in your workflow. For example
550
+
551
+ workflow do
552
+ state :first
553
+ state :second do
554
+ event :backward, :transitions_to => :first
555
+ end
556
+
557
+ on_unavailable_transition do |from, to_name, *args|
558
+ Log.warn "Workflow: #{from} does not have #{to_name} available to it"
559
+ end
560
+ end
561
+
562
+ If backward! is called when in the `first` state, 'on_unavailable_transition' is invoked and workflow stays in a 'first' state. This
563
+ example surpresses the Workflow::NoTransitionAllowed exception from being raised, if you still want it to be raised you can simply
564
+ call it yourself or return false.
565
+
566
+ This is particularly useful when you don't want a processes to be aborted due to the workflow being in an unexpected state.
567
+
568
+ ### Guards
569
+
570
+ If you want to halt the transition conditionally, you can just raise an
571
+ exception in your [transition event handler](#transition_event_handler).
572
+ There is a helper called `halt!`, which raises the
573
+ Workflow::TransitionHalted exception. You can provide an additional
574
+ `halted_because` parameter.
575
+
576
+ ```ruby
577
+ def reject(reason)
578
+ halt! 'We do not reject articles unless the reason is important' \
579
+ unless reason =~ /important/i
580
+ end
581
+ ```
582
+
583
+ The traditional `halt` (without the exclamation mark) is still supported
584
+ too. This just prevents the state change without raising an
585
+ exception.
586
+
587
+ You can check `halted?` and `halted_because` values later.
588
+
589
+ ### Hook order
590
+
591
+ The whole event sequence is as follows:
592
+
593
+ * `before_transition`
594
+ * event specific action
595
+ * `on_transition` (if action did not halt)
596
+ * `on_exit`
597
+ * PERSIST WORKFLOW STATE, i.e. transition
598
+ * `on_entry`
599
+ * `after_transition`
600
+
601
+
602
+ Multiple Workflows
603
+ ------------------
604
+
605
+ I am frequently asked if it's possible to represent multiple "workflows"
606
+ in an ActiveRecord class.
607
+
608
+ The solution depends on your business logic and how you want to
609
+ structure your implementation.
610
+
611
+ ### Use Single Table Inheritance
612
+
613
+ One solution can be to do it on the class level and use a class
614
+ hierarchy. You can use [single table inheritance][STI] so there is only
615
+ single `orders` table in the database. Read more in the chapter "Single
616
+ Table Inheritance" of the [ActiveRecord documentation][ActiveRecord].
617
+ Then you define your different classes:
618
+
619
+ ```ruby
620
+ class Order < ActiveRecord::Base
621
+ include Workflow
622
+ end
623
+
624
+ class SmallOrder < Order
625
+ workflow do
626
+ # workflow definition for small orders goes here
627
+ end
628
+ end
629
+
630
+ class BigOrder < Order
631
+ workflow do
632
+ # workflow for big orders, probably with a longer approval chain
633
+ end
634
+ end
635
+ ```
636
+
637
+ ### Individual workflows for objects
638
+
639
+ Another solution would be to connect different workflows to object
640
+ instances via metaclass, e.g.
641
+
642
+ ```ruby
643
+ # Load an object from the database
644
+ booking = Booking.find(1234)
645
+
646
+ # Now define a workflow - exclusively for this object,
647
+ # probably depending on some condition or database field
648
+ if # some condition
649
+ class << booking
650
+ include Workflow
651
+ workflow do
652
+ state :state1
653
+ state :state2
654
+ end
655
+ end
656
+ # if some other condition, use a different workflow
657
+ ```
658
+
659
+ You can also encapsulate this in a class method or even put in some
660
+ ActiveRecord callback. Please also have a look at [the full working
661
+ example][multiple_workflow_test]!
662
+
663
+ [STI]: http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
664
+ [ActiveRecord]: http://api.rubyonrails.org/classes/ActiveRecord/Base.html
665
+ [multiple_workflow_test]: http://github.com/geekq/workflow/blob/master/test/multiple_workflows_test.rb
666
+
667
+
668
+ Documenting with diagrams
669
+ -------------------------
670
+
671
+ You can generate a graphical representation of the workflow for
672
+ a particular class for documentation purposes.
673
+ Use `Workflow::create_workflow_diagram(class)` in your rake task like:
674
+
675
+ ```ruby
676
+ namespace :doc do
677
+ desc "Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."
678
+ task :workflow => :environment do
679
+ require 'workflow/draw'
680
+ Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)
681
+ end
682
+ end
683
+ ```
684
+
685
+ About
686
+ -----
687
+
688
+ Workflow Orchestrator is maintained by [Lorefnon](https://lorefnon.me) along with [many contributors](https://github.com/lorefnon/workflow-orchestrator/graphs/contributors).
689
+
690
+ This project was derived (forked) from the gem [geekq/workflow](https://github.com/geekq/workflow) by Vladimir Dobriakov, which was forked from the original repo authored by Ryan Allen. Both appear to be unmaintained as of 2016.
691
+
692
+ While it is largely compatible with geekq/workflow but breaking API changes will be introduced in coming versions. In addition, the intent is to extract the persistence and rails dependent features in different gems, leaving only the FSM management features in the core.
693
+
694
+ History
695
+ -------
696
+
697
+ Copyright (c) 2016 Lorefnon
698
+
699
+ Copyright (c) 2010-2014 Vladimir Dobriakov
700
+
701
+ Copyright (c) 2008-2009 Vodafone
702
+
703
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
704
+
705
+ Based on the work of Ryan Allen and Scott Barron
706
+
707
+ Licensed under MIT license, see the MIT-LICENSE file.