workflow 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ tmp
2
+ nbproject
3
+ pkg
4
+ doc/
5
+ html/
6
+ *.swp
7
+ *.gemspec
data/README.markdown ADDED
@@ -0,0 +1,433 @@
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
+
77
+ Installation
78
+ ------------
79
+
80
+ gem install workflow
81
+
82
+ Alternatively you can just download the lib/workflow.rb and put it in
83
+ the lib folder of your Rails or Ruby application.
84
+
85
+
86
+ Examples
87
+ --------
88
+
89
+ After installation or downloading of the library you can easily try out
90
+ all the example code from this README in irb.
91
+
92
+ $ irb
93
+ require 'rubygems'
94
+ require 'workflow'
95
+
96
+ Now just copy and paste the source code from the beginning of this README
97
+ file snippet by snippet and observe the output.
98
+
99
+
100
+ Transition event handler
101
+ ------------------------
102
+
103
+ The best way is to use convention over configuration and to define a
104
+ method with the same name as the event. Then it is automatically invoked
105
+ when event is raised. For the Article workflow defined earlier it would
106
+ be:
107
+
108
+ class Article
109
+ def reject
110
+ puts 'sending email to the author explaining the reason...'
111
+ end
112
+ end
113
+
114
+ `article.review!; article.reject!` will cause a state transition, persist the new state
115
+ (if integrated with ActiveRecord) and invoke this user defined reject
116
+ method.
117
+
118
+ You can also define event handler accepting/requiring additional
119
+ arguments:
120
+
121
+ class Article
122
+ def review(reviewer = '')
123
+ puts "[#{reviewer}] is now reviewing the article"
124
+ end
125
+ end
126
+
127
+ article2 = Article.new
128
+ article2.submit!
129
+ article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
130
+
131
+
132
+ ### The old, deprecated way
133
+
134
+ The old way, using a block is still supported but deprecated:
135
+
136
+ event :review, :transitions_to => :being_reviewed do |reviewer|
137
+ # store the reviewer
138
+ end
139
+
140
+ We've noticed, that mixing the list of events and states with the blocks
141
+ invoked for particular transitions leads to a bumpy and poorly readable code
142
+ due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
143
+ we decided to invoke an optional user defined callback method with the same
144
+ name as the event (convention over configuration) as explained before.
145
+
146
+
147
+ Integration with ActiveRecord
148
+ -----------------------------
149
+
150
+ Workflow library can handle the state persistence fully automatically. You
151
+ only need to define a string field on the table called `workflow_state`
152
+ and include the workflow mixin in your model class as usual:
153
+
154
+ class Order < ActiveRecord::Base
155
+ include Workflow
156
+ workflow do
157
+ # list states and transitions here
158
+ end
159
+ end
160
+
161
+ On a database record loading all the state check methods e.g.
162
+ `article.state`, `article.awaiting_review?` are immediately available.
163
+ For new records or if the workflow_state field is not set the state
164
+ defaults to the first state declared in the workflow specification. In
165
+ our example it is `:new`, so `Article.new.new?` returns true and
166
+ `Article.new.approved?` returns false.
167
+
168
+ At the end of a successful state transition like `article.approve!` the
169
+ new state is immediately saved in the database.
170
+
171
+ You can change this behaviour by overriding `persist_workflow_state`
172
+ method.
173
+
174
+
175
+ ### Custom workflow database column
176
+
177
+ [meuble](http://imeuble.info/) contributed a solution for using
178
+ custom persistence column easily, e.g. for a legacy database schema:
179
+
180
+ class LegacyOrder < ActiveRecord::Base
181
+ include Workflow
182
+
183
+ workflow_column :foo_bar # use this legacy database column for
184
+ # persistence
185
+ end
186
+
187
+
188
+
189
+ ### Single table inheritance
190
+
191
+ Single table inheritance is also supported. Descendant classes can either
192
+ inherit the workflow definition from the parent or override with its own
193
+ definition.
194
+
195
+ Custom workflow state persistence
196
+ ---------------------------------
197
+
198
+ If you do not use a relational database and ActiveRecord, you can still
199
+ integrate the workflow very easily. To implement persistence you just
200
+ need to override `load_workflow_state` and
201
+ `persist_workflow_state(new_value)` methods. Next section contains an example for
202
+ using CouchDB, a document oriented database.
203
+
204
+ [Tim Lossen](http://tim.lossen.de/) implemented support
205
+ for [remodel](http://github.com/tlossen/remodel) / [redis](http://github.com/antirez/redis)
206
+ key-value store.
207
+
208
+ Integration with CouchDB
209
+ ------------------------
210
+
211
+ We are using the compact [couchtiny library](http://github.com/geekq/couchtiny)
212
+ here. But the implementation would look similar for the popular
213
+ couchrest library.
214
+
215
+ require 'couchtiny'
216
+ require 'couchtiny/document'
217
+ require 'workflow'
218
+
219
+ class User < CouchTiny::Document
220
+ include Workflow
221
+ workflow do
222
+ state :submitted do
223
+ event :activate_via_link, :transitions_to => :proved_email
224
+ end
225
+ state :proved_email
226
+ end
227
+
228
+ def load_workflow_state
229
+ self[:workflow_state]
230
+ end
231
+
232
+ def persist_workflow_state(new_value)
233
+ self[:workflow_state] = new_value
234
+ save!
235
+ end
236
+ end
237
+
238
+ Please also have a look at
239
+ [the full source code](http://github.com/geekq/workflow/blob/master/test/couchtiny_example.rb).
240
+
241
+ Accessing your workflow specification
242
+ -------------------------------------
243
+
244
+ You can easily reflect on workflow specification programmatically - for
245
+ the whole class or for the current object. Examples:
246
+
247
+ article2.current_state.events # lists possible events from here
248
+ article2.current_state.events[:reject].transitions_to # => :rejected
249
+
250
+ Article.workflow_spec.states.keys
251
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
252
+
253
+ Article.workflow_spec.state_names
254
+ #=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
255
+
256
+ # list all events for all states
257
+ Article.workflow_spec.states.values.collect &:events
258
+
259
+
260
+ You can also store and later retrieve additional meta data for every
261
+ state and every event:
262
+
263
+ class MyProcess
264
+ include Workflow
265
+ workflow do
266
+ state :main, :meta => {:importance => 8}
267
+ state :supplemental, :meta => {:importance => 1}
268
+ end
269
+ end
270
+ puts MyProcess.workflow_spec.states[:supplemental].meta[:importance] # => 1
271
+
272
+ The workflow library itself uses this feature to tweak the graphical
273
+ representation of the workflow. See below.
274
+
275
+
276
+ Advanced transition hooks
277
+ -------------------------
278
+
279
+ ### on_entry/on_exit
280
+
281
+ We already had a look at the declaring callbacks for particular workflow
282
+ events. If you would like to react to all transitions to/from the same state
283
+ in the same way you can use the on_entry/on_exit hooks. You can either define it
284
+ with a block inside the workflow definition or through naming
285
+ convention, e.g. for the state :pending just define the method
286
+ `on_pending_exit(new_state, event, *args)` somewhere in your class.
287
+
288
+ ### on_transition
289
+
290
+ If you want to be informed about everything happening everywhere, e.g. for
291
+ logging then you can use the universal `on_transition` hook:
292
+
293
+ workflow do
294
+ state :one do
295
+ event :increment, :transitions_to => :two
296
+ end
297
+ state :two
298
+ on_transition do |from, to, triggering_event, *event_args|
299
+ Log.info "#{from} -> #{to}"
300
+ end
301
+ end
302
+
303
+
304
+ ### Guards
305
+
306
+ If you want to halt the transition conditionally, you can just raise an
307
+ exception. There is a helper called `halt!`, which raises the
308
+ Workflow::TransitionHalted exception. You can provide an additional
309
+ `halted_because` parameter.
310
+
311
+ def reject(reason)
312
+ halt! 'We do not reject articles unless the reason is important' \
313
+ unless reason =~ /important/i
314
+ end
315
+
316
+ The traditional `halt` (without the exclamation mark) is still supported
317
+ too. This just prevents the state change without raising an
318
+ exception.
319
+
320
+ ### Hook order
321
+
322
+ The whole event sequence is as follows:
323
+
324
+ * event specific action
325
+ * on_transition (if action did not halt)
326
+ * on_exit
327
+ * PERSIST WORKFLOW STATE, i.e. transition
328
+ * on_entry
329
+
330
+
331
+ Documenting with diagrams
332
+ -------------------------
333
+
334
+ You can generate a graphical representation of your workflow for
335
+ documentation purposes. S. Workflow::create_workflow_diagram.
336
+
337
+
338
+ Earlier versions
339
+ ----------------
340
+
341
+ The `workflow` library was originally written by Ryan Allen.
342
+
343
+ The version 0.3 was almost completely (including ActiveRecord
344
+ integration, API for accessing workflow specification,
345
+ method_missing free implementation) rewritten by Vladimir Dobriakov
346
+ keeping the original workflow DSL spirit.
347
+
348
+
349
+ Migration from the original Ryan's library
350
+ ------------------------------------------
351
+
352
+ Credit: Michael (rockrep)
353
+
354
+ Accessing workflow specification
355
+
356
+ my_instance.workflow # old
357
+ MyClass.workflow_spec # new
358
+
359
+ Accessing states, events, meta, e.g.
360
+
361
+ my_instance.workflow.states(:some_state).events(:some_event).meta[:some_meta_tag] # old
362
+ MyClass.workflow_spec.states[:some_state].events[:some_event].meta[:some_meta_tag] # new
363
+
364
+ Causing state transitions
365
+
366
+ my_instance.workflow.my_event # old
367
+ my_instance.my_event! # new
368
+
369
+ when using both a block and a callback method for an event, the block executes prior to the callback
370
+
371
+
372
+ Changelog
373
+ ---------
374
+
375
+ ### New in the version 0.4.0
376
+
377
+ * completely rewritten the documentation to match my branch
378
+ * switch to [jeweler][] for building gems
379
+ * every described feature is backed up by an automated test
380
+
381
+ ### New in the version 0.3.0
382
+
383
+ Intermixing of transition graph definition (states, transitions)
384
+ on the one side and implementation of the actions on the other side
385
+ for a bigger state machine can introduce clutter.
386
+
387
+ To reduce this clutter it is now possible to use state entry- and
388
+ exit- hooks defined through a naming convention. For example, if there
389
+ is a state :pending, then instead of using a
390
+ block:
391
+
392
+ state :pending do
393
+ on_entry do
394
+ # your implementation here
395
+ end
396
+ end
397
+
398
+ you can hook in by defining method
399
+
400
+ def on_pending_exit(new_state, event, *args)
401
+ # your implementation here
402
+ end
403
+
404
+ anywhere in your class. You can also use a simpler function signature
405
+ like `def on_pending_exit(*args)` if your are not interested in
406
+ arguments. Please note: `def on_pending_exit()` with an empty list
407
+ would not work.
408
+
409
+ If both a function with a name according to naming convention and the
410
+ on_entry/on_exit block are given, then only on_entry/on_exit block is used.
411
+
412
+
413
+ Support
414
+ -------
415
+
416
+ ### Reporting bugs
417
+
418
+ http://github.com/geekq/workflow/issues
419
+
420
+
421
+ About
422
+ -----
423
+
424
+ Author: Vladimir Dobriakov, http://www.innoq.com/blog/vd, http://blog.geekq.net/
425
+
426
+ Copyright (c) 2008-2009 Vodafone
427
+
428
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
429
+
430
+ Based on the work of Ryan Allen and Scott Barron
431
+
432
+ Licensed under MIT license, see the MIT-LICENSE file.
433
+
data/Rakefile CHANGED
@@ -11,34 +11,34 @@ Rake::TestTask.new do |t|
11
11
  t.pattern = 'test/*_test.rb'
12
12
  end
13
13
 
14
- PKG_VERSION = "0.3.0"
15
- PKG_FILES = FileList[
16
- 'MIT-LICENSE',
17
- 'README.rdoc',
18
- 'Rakefile',
19
- 'lib/**/*.rb',
20
- 'test/**/test_*.rb'
21
- ]
22
-
23
- spec = Gem::Specification.new do |s|
24
- s.name = "workflow"
25
- s.version = PKG_VERSION
26
- s.author = "Vladimir Dobriakov"
27
- s.email = "vladimir@geekq.net"
28
- s.homepage = "http://blog.geekQ.net/"
29
- s.platform = Gem::Platform::RUBY
30
- s.summary = "A replacement for acts_as_state_machine."
31
- s.files = PKG_FILES.to_a
32
- s.require_path = "lib"
33
- end
34
-
35
14
  Rake::RDocTask.new do |rdoc|
36
- rdoc.main = "README"
37
- rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
15
+ rdoc.rdoc_files.include("lib/**/*.rb")
38
16
  rdoc.options << "-S"
39
17
  end
40
18
 
41
- package_task = Rake::GemPackageTask.new(spec) do |pkg|
42
- pkg.need_zip = true
43
- pkg.need_tar_gz = true
19
+ begin
20
+ require 'jeweler'
21
+ Jeweler::Tasks.new do |gemspec|
22
+ gemspec.name = "workflow"
23
+ gemspec.rubyforge_project = 'workflow'
24
+ gemspec.email = "vladimir@geekq.net"
25
+ gemspec.homepage = "http://blog.geekQ.net/"
26
+ gemspec.authors = ["Vladimir Dobriakov"]
27
+ gemspec.summary = "A replacement for acts_as_state_machine."
28
+ gemspec.description = <<-EOS
29
+ Workflow is a finite-state-machine-inspired API for modeling and interacting
30
+ with what we tend to refer to as 'workflow'.
31
+
32
+ * nice DSL to describe your states, events and transitions
33
+ * robust integration with ActiveRecord and non relational data stores
34
+ * various hooks for single transitions, entering state etc.
35
+ * convenient access to the workflow specification: list states, possible events
36
+ for particular state
37
+ EOS
38
+
39
+ Jeweler::GemcutterTasks.new
40
+ end
41
+ rescue LoadError
42
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
44
43
  end
44
+