nimboids-workflow 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ tmp
2
+ nbproject
3
+ pkg
4
+ doc/
5
+ html/
6
+ *.swp
7
+ *.gemspec
8
+ .yardoc
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Vodafone
2
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
@@ -0,0 +1,550 @@
1
+ What is workflow?
2
+ -----------------
3
+
4
+ Workflow is a finite-state-machine-inspired API for modeling and
5
+ interacting with what we tend to refer to as 'workflow'.
6
+
7
+ A lot of business modeling tends to involve workflow-like concepts, and
8
+ the aim of this library is to make the expression of these concepts as
9
+ clear as possible, using similar terminology as found in state machine
10
+ theory.
11
+
12
+ So, a workflow has a state. It can only be in one state at a time. When
13
+ a workflow changes state, we call that a transition. Transitions occur
14
+ on an event, so events cause transitions to occur. Additionally, when an
15
+ event fires, other arbitrary code can be executed, we call those actions.
16
+ So any given state has a bunch of events, any event in a state causes a
17
+ transition to another state and potentially causes code to be executed
18
+ (an action). We can hook into states when they are entered, and exited
19
+ from, and we can cause transitions to fail (guards), and we can hook in
20
+ to every transition that occurs ever for whatever reason we can come up
21
+ with.
22
+
23
+ Now, all that's a mouthful, but we'll demonstrate the API bit by bit
24
+ with a real-ish world example.
25
+
26
+ Let's say we're modeling article submission from journalists. An article
27
+ is written, then submitted. When it's submitted, it's awaiting review.
28
+ Someone reviews the article, and then either accepts or rejects it.
29
+ Here is the expression of this workflow using the API:
30
+
31
+ class Article
32
+ include Workflow
33
+ workflow do
34
+ state :new do
35
+ event :submit, :transitions_to => :awaiting_review
36
+ end
37
+ state :awaiting_review do
38
+ event :review, :transitions_to => :being_reviewed
39
+ end
40
+ state :being_reviewed do
41
+ event :accept, :transitions_to => :accepted
42
+ event :reject, :transitions_to => :rejected
43
+ end
44
+ state :accepted
45
+ state :rejected
46
+ end
47
+ end
48
+
49
+ Nice, isn't it!
50
+
51
+ Let's create an article instance and check in which state it is:
52
+
53
+ article = Article.new
54
+ article.accepted? # => false
55
+ article.new? # => true
56
+
57
+ You can also access the whole `current_state` object including the list
58
+ of possible events and other meta information:
59
+
60
+ article.current_state
61
+ => #<Workflow::State:0x7f1e3d6731f0 @events={
62
+ :submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
63
+ @transitions_to=:awaiting_review, @name=:submit, @meta={}>},
64
+ name:new, meta{}
65
+
66
+ Now we can call the submit event, which transitions to the
67
+ <tt>:awaiting_review</tt> state:
68
+
69
+ article.submit!
70
+ article.awaiting_review? # => true
71
+
72
+ Events are actually instance methods on a workflow, and depending on the
73
+ state you're in, you'll have a different set of events used to
74
+ transition to other states.
75
+
76
+ It is also easy to check, if a certain transition is possible from the
77
+ current state . `article.can_submit?` checks if there is a `:submit`
78
+ event (transition) defined for the current state.
79
+
80
+
81
+ Installation
82
+ ------------
83
+
84
+ gem install workflow
85
+
86
+ Alternatively you can just download the lib/workflow.rb and put it in
87
+ the lib folder of your Rails or Ruby application.
88
+
89
+
90
+ Ruby 1.9
91
+ --------
92
+
93
+ Workflow gem does not work with some (but very widespread) Ruby 1.9
94
+ builds due to a known bug in Ruby 1.9. Either compile your Ruby 1.9 from
95
+ source or [comment out some lines in workflow](http://github.com/geekq/workflow/issues#issue/6)
96
+ (reduces functionality).
97
+
98
+ Examples
99
+ --------
100
+
101
+ After installation or downloading of the library you can easily try out
102
+ all the example code from this README in irb.
103
+
104
+ $ irb
105
+ require 'rubygems'
106
+ require 'workflow'
107
+
108
+ Now just copy and paste the source code from the beginning of this README
109
+ file snippet by snippet and observe the output.
110
+
111
+
112
+ Transition event handler
113
+ ------------------------
114
+
115
+ The best way is to use convention over configuration and to define a
116
+ method with the same name as the event. Then it is automatically invoked
117
+ when event is raised. For the Article workflow defined earlier it would
118
+ be:
119
+
120
+ class Article
121
+ def reject
122
+ puts 'sending email to the author explaining the reason...'
123
+ end
124
+ end
125
+
126
+ `article.review!; article.reject!` will cause a state transition, persist the new state
127
+ (if integrated with ActiveRecord) and invoke this user defined reject
128
+ method.
129
+
130
+ You can also define event handler accepting/requiring additional
131
+ arguments:
132
+
133
+ class Article
134
+ def review(reviewer = '')
135
+ puts "[#{reviewer}] is now reviewing the article"
136
+ end
137
+ end
138
+
139
+ article2 = Article.new
140
+ article2.submit!
141
+ article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
142
+
143
+
144
+ ### The old, deprecated way
145
+
146
+ The old way, using a block is still supported but deprecated:
147
+
148
+ event :review, :transitions_to => :being_reviewed do |reviewer|
149
+ # store the reviewer
150
+ end
151
+
152
+ We've noticed, that mixing the list of events and states with the blocks
153
+ invoked for particular transitions leads to a bumpy and poorly readable code
154
+ due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
155
+ we decided to invoke an optional user defined callback method with the same
156
+ name as the event (convention over configuration) as explained before.
157
+
158
+
159
+ Integration with ActiveRecord
160
+ -----------------------------
161
+
162
+ Workflow library can handle the state persistence fully automatically. You
163
+ only need to define a string field on the table called `workflow_state`
164
+ and include the workflow mixin in your model class as usual:
165
+
166
+ class Order < ActiveRecord::Base
167
+ include Workflow
168
+ workflow do
169
+ # list states and transitions here
170
+ end
171
+ end
172
+
173
+ On a database record loading all the state check methods e.g.
174
+ `article.state`, `article.awaiting_review?` are immediately available.
175
+ For new records or if the workflow_state field is not set the state
176
+ defaults to the first state declared in the workflow specification. In
177
+ our example it is `:new`, so `Article.new.new?` returns true and
178
+ `Article.new.approved?` returns false.
179
+
180
+ At the end of a successful state transition like `article.approve!` the
181
+ new state is immediately saved in the database.
182
+
183
+ You can change this behaviour by overriding `persist_workflow_state`
184
+ method.
185
+
186
+
187
+ ### Custom workflow database column
188
+
189
+ [meuble](http://imeuble.info/) contributed a solution for using
190
+ custom persistence column easily, e.g. for a legacy database schema:
191
+
192
+ class LegacyOrder < ActiveRecord::Base
193
+ include Workflow
194
+
195
+ workflow_column :foo_bar # use this legacy database column for
196
+ # persistence
197
+ end
198
+
199
+
200
+
201
+ ### Single table inheritance
202
+
203
+ Single table inheritance is also supported. Descendant classes can either
204
+ inherit the workflow definition from the parent or override with its own
205
+ definition.
206
+
207
+ Custom workflow state persistence
208
+ ---------------------------------
209
+
210
+ If you do not use a relational database and ActiveRecord, you can still
211
+ integrate the workflow very easily. To implement persistence you just
212
+ need to override `load_workflow_state` and
213
+ `persist_workflow_state(new_value)` methods. Next section contains an example for
214
+ using CouchDB, a document oriented database.
215
+
216
+ [Tim Lossen](http://tim.lossen.de/) implemented support
217
+ for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
218
+ key-value store.
219
+
220
+ Integration with CouchDB
221
+ ------------------------
222
+
223
+ We are using the compact [couchtiny library](http://github.com/geekq/couchtiny)
224
+ here. But the implementation would look similar for the popular
225
+ couchrest library.
226
+
227
+ require 'couchtiny'
228
+ require 'couchtiny/document'
229
+ require 'workflow'
230
+
231
+ class User < CouchTiny::Document
232
+ include Workflow
233
+ workflow do
234
+ state :submitted do
235
+ event :activate_via_link, :transitions_to => :proved_email
236
+ end
237
+ state :proved_email
238
+ end
239
+
240
+ def load_workflow_state
241
+ self[:workflow_state]
242
+ end
243
+
244
+ def persist_workflow_state(new_value)
245
+ self[:workflow_state] = new_value
246
+ save!
247
+ end
248
+ end
249
+
250
+ Please also have a look at
251
+ [the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
252
+
253
+ Integration with Mongoid
254
+ ------------------------
255
+
256
+ You can integrate with Mongoid following the example above for CouchDB, but there is a gem that does that for you (and includes extensive tests):
257
+ [workflow_on_mongoid](http://github.com/bowsersenior/workflow_on_mongoid)
258
+
259
+ Accessing your workflow specification
260
+ -------------------------------------
261
+
262
+ You can easily reflect on workflow specification programmatically - for
263
+ the whole class or for the current object. Examples:
264
+
265
+ article2.current_state.events # lists possible events from here
266
+ article2.current_state.events[:reject].transitions_to # => :rejected
267
+
268
+ Article.workflow_spec.states.keys
269
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
270
+
271
+ Article.workflow_spec.state_names
272
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
273
+
274
+ # list all events for all states
275
+ Article.workflow_spec.states.values.collect &:events
276
+
277
+
278
+ You can also store and later retrieve additional meta data for every
279
+ state and every event:
280
+
281
+ class MyProcess
282
+ include Workflow
283
+ workflow do
284
+ state :main, :meta => {:importance => 8}
285
+ state :supplemental, :meta => {:importance => 1}
286
+ end
287
+ end
288
+ puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
289
+
290
+ The workflow library itself uses this feature to tweak the graphical
291
+ representation of the workflow. See below.
292
+
293
+
294
+ Advanced transition hooks
295
+ -------------------------
296
+
297
+ ### on_entry/on_exit
298
+
299
+ We already had a look at the declaring callbacks for particular workflow
300
+ events. If you would like to react to all transitions to/from the same state
301
+ in the same way you can use the on_entry/on_exit hooks. You can either define it
302
+ with a block inside the workflow definition or through naming
303
+ convention, e.g. for the state :pending just define the method
304
+ `on_pending_exit(new_state, event, *args)` somewhere in your class.
305
+
306
+ ### on_transition
307
+
308
+ If you want to be informed about everything happening everywhere, e.g. for
309
+ logging then you can use the universal `on_transition` hook:
310
+
311
+ workflow do
312
+ state :one do
313
+ event :increment, :transitions_to => :two
314
+ end
315
+ state :two
316
+ on_transition do |from, to, triggering_event, *event_args|
317
+ Log.info "#{from} -> #{to}"
318
+ end
319
+ end
320
+
321
+ Please also have a look at the [advanced end to end
322
+ example][advanced_hooks_and_validation_test].
323
+
324
+ [advanced_hooks_and_validation_test]: http://github.com/geekq/workflow/blob/master/test/advanced_hooks_and_validation_test.rb
325
+
326
+ ### Guards
327
+
328
+ If you want to halt the transition conditionally, you can just raise an
329
+ exception in your [transition event handler](#transition_event_handler).
330
+ There is a helper called `halt!`, which raises the
331
+ Workflow::TransitionHalted exception. You can provide an additional
332
+ `halted_because` parameter.
333
+
334
+ def reject(reason)
335
+ halt! 'We do not reject articles unless the reason is important' \
336
+ unless reason =~ /important/i
337
+ end
338
+
339
+ The traditional `halt` (without the exclamation mark) is still supported
340
+ too. This just prevents the state change without raising an
341
+ exception.
342
+
343
+ You can check `halted?` and `halted_because` values later.
344
+
345
+ ### Hook order
346
+
347
+ The whole event sequence is as follows:
348
+
349
+ * event specific action
350
+ * on_transition (if action did not halt)
351
+ * on_exit
352
+ * PERSIST WORKFLOW STATE, i.e. transition
353
+ * on_entry
354
+
355
+
356
+ Multiple Workflows
357
+ ------------------
358
+
359
+ I am frequently asked if it's possible to represent multiple "workflows"
360
+ in an ActiveRecord class.
361
+
362
+ The solution depends on your business logic and how you want to
363
+ structure your implementation.
364
+
365
+ ### Use Single Table Inheritance
366
+
367
+ One solution can be to do it on the class level and use a class
368
+ hierarchy. You can use [single table inheritance][STI] so there is only
369
+ single `orders` table in the database. Read more in the chapter "Single
370
+ Table Inheritance" of the [ActiveRecord documentation][ActiveRecord].
371
+ Then you define your different classes:
372
+
373
+ class Order < ActiveRecord::Base
374
+ include Workflow
375
+ end
376
+
377
+ class SmallOrder < Order
378
+ workflow do
379
+ # workflow definition for small orders goes here
380
+ end
381
+ end
382
+
383
+ class BigOrder < Order
384
+ workflow do
385
+ # workflow for big orders, probably with a longer approval chain
386
+ end
387
+ end
388
+
389
+
390
+ ### Individual workflows for objects
391
+
392
+ Another solution would be to connect different workflows to object
393
+ instances via metaclass, e.g.
394
+
395
+ # Load an object from the database
396
+ booking = Booking.find(1234)
397
+
398
+ # Now define a workflow - exclusively for this object,
399
+ # probably depending on some condition or database field
400
+ if # some condition
401
+ class << booking
402
+ include Workflow
403
+ workflow do
404
+ state :state1
405
+ state :state2
406
+ end
407
+ end
408
+ # if some other condition, use a different workflow
409
+
410
+ You can also encapsulate this in a class method or even put in some
411
+ ActiveRecord callback. Please also have a look at [the full working
412
+ example][multiple_workflow_test]!
413
+
414
+ [STI]: http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
415
+ [ActiveRecord]: http://api.rubyonrails.org/classes/ActiveRecord/Base.html
416
+ [multiple_workflow_test]: http://github.com/geekq/workflow/blob/master/test/multiple_workflows_test.rb
417
+
418
+
419
+ Documenting with diagrams
420
+ -------------------------
421
+
422
+ You can generate a graphical representation of your workflow for
423
+ documentation purposes. S. Workflow::create_workflow_diagram.
424
+
425
+
426
+ Earlier versions
427
+ ----------------
428
+
429
+ The `workflow` library was originally written by Ryan Allen.
430
+
431
+ The version 0.3 was almost completely (including ActiveRecord
432
+ integration, API for accessing workflow specification,
433
+ method_missing free implementation) rewritten by Vladimir Dobriakov
434
+ keeping the original workflow DSL spirit.
435
+
436
+
437
+ Migration from the original Ryan's library
438
+ ------------------------------------------
439
+
440
+ Credit: Michael (rockrep)
441
+
442
+ Accessing workflow specification
443
+
444
+ my_instance.workflow # old
445
+ MyClass.workflow_spec # new
446
+
447
+ Accessing states, events, meta, e.g.
448
+
449
+ my_instance.workflow.states(:some_state).events(:some_event).meta[:some_meta_tag] # old
450
+ MyClass.workflow_spec.states[:some_state].events[:some_event].meta[:some_meta_tag] # new
451
+
452
+ Causing state transitions
453
+
454
+ my_instance.workflow.my_event # old
455
+ my_instance.my_event! # new
456
+
457
+ when using both a block and a callback method for an event, the block executes prior to the callback
458
+
459
+
460
+ Changelog
461
+ ---------
462
+
463
+ ### New in the version 0.8.0
464
+
465
+ * check if a certain transition possible from the current state with
466
+ `can_....?`
467
+ * fix workflow_state persistence for multiple_workflows example
468
+
469
+ ### New in the version 0.7.0
470
+
471
+ * fix issue#10 Workflow::create_workflow_diagram documentation and path
472
+ escaping
473
+ * fix issue#7 workflow_column does not work STI (single table
474
+ inheritance) ActiveRecord models
475
+ * fix issue#5 Diagram generation fails for models in modules
476
+
477
+ ### New in the version 0.6.0
478
+
479
+ * enable multiple workflows by connecting workflow to object instances
480
+ (using metaclass) instead of connecting to a class, s. "Multiple
481
+ Workflows" section
482
+
483
+ ### New in the version 0.5.0
484
+
485
+ * fix issue#3 change the behaviour of halt! to immediately raise an
486
+ exception. See also http://github.com/geekq/workflow/issues/#issue/3
487
+
488
+ ### New in the version 0.4.0
489
+
490
+ * completely rewritten the documentation to match my branch
491
+ * switch to [jeweler][] for building gems
492
+ * use [gemcutter][] for gem distribution
493
+ * every described feature is backed up by an automated test
494
+
495
+ [jeweler]: http://github.com/technicalpickles/jeweler
496
+ [gemcutter]: http://gemcutter.org/gems/workflow
497
+
498
+ ### New in the version 0.3.0
499
+
500
+ Intermixing of transition graph definition (states, transitions)
501
+ on the one side and implementation of the actions on the other side
502
+ for a bigger state machine can introduce clutter.
503
+
504
+ To reduce this clutter it is now possible to use state entry- and
505
+ exit- hooks defined through a naming convention. For example, if there
506
+ is a state :pending, then instead of using a
507
+ block:
508
+
509
+ state :pending do
510
+ on_entry do
511
+ # your implementation here
512
+ end
513
+ end
514
+
515
+ you can hook in by defining method
516
+
517
+ def on_pending_exit(new_state, event, *args)
518
+ # your implementation here
519
+ end
520
+
521
+ anywhere in your class. You can also use a simpler function signature
522
+ like `def on_pending_exit(*args)` if your are not interested in
523
+ arguments. Please note: `def on_pending_exit()` with an empty list
524
+ would not work.
525
+
526
+ If both a function with a name according to naming convention and the
527
+ on_entry/on_exit block are given, then only on_entry/on_exit block is used.
528
+
529
+
530
+ Support
531
+ -------
532
+
533
+ ### Reporting bugs
534
+
535
+ <http://github.com/geekq/workflow/issues>
536
+
537
+
538
+ About
539
+ -----
540
+
541
+ Author: Vladimir Dobriakov, <http://www.innoq.com/blog/vd>, <http://blog.geekq.net/>
542
+
543
+ Copyright (c) 2008-2009 Vodafone
544
+
545
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
546
+
547
+ Based on the work of Ryan Allen and Scott Barron
548
+
549
+ Licensed under MIT license, see the MIT-LICENSE file.
550
+