railsware-workflow 0.8.1

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