workflow 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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
+