Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 298 lines (217 sloc) 13.032 kb
7fc69c8 @robolson Moved README to README.rdoc
robolson authored
1 = Workflow
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
2
0fc381d @ryan-allen point to geekq's maintained fork of workflow
authored
3 === THIS COPY IS UNMAINTAINED: LINK TO ACTIVE FORK!
4
5 Hello! This copy of Workflow isn't currently being maintained. The active fork is located here:
6
7 http://github.com/geekq/workflow/tree/master
8
9 Cheers!
10
11 === MAILING LIST!
39155e0 @ryan-allen WORKFLOW HAS A MAILING LIST (so just, like, letting you all know) :)
authored
12
13 Hi! We've now got a mailing list to talk about Workflow, and that's good! Come visit and post your problems or ideas or anything!!!
14
15 http://groups.google.com/group/ruby-workflow
16
17 See you there!
18
7fc69c8 @robolson Moved README to README.rdoc
robolson authored
19 === What is workflow?
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
20
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
21 Workflow is a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as 'workflow'.
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
22
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
23 A lot of business modeling tends to involve workflow-like concepts, and the aim of this library is to make the expression of these concepts as clear as possible, using similar terminology as found in state machine theory.
b07506e @ryan-allen moar updates on state machine
authored
24
9cba632 @ryan-allen moar for README, we're done, just need to proof and rewrite in a consist...
authored
25 So, a workflow has a state. It can only be in one state at a time. When a workflow changes state, we call that a transition. Transitions occur on an event, so events cause transitions to occur. Additionally, when an event fires, other random code can be executed, we call those actions. So any given state has a bunch of events, any event in a state causes a transition to another state and potentially causes code to be executed (an action). 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.
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
26
9cba632 @ryan-allen moar for README, we're done, just need to proof and rewrite in a consist...
authored
27 Now, all that's a mouthful, but we'll demonstrate the API bit by bit with a real-ish world example.
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
28
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
29 Let's say we're modeling article submission from journalists. An article is written, then submitted. When it's submitted, it's awaiting review. Someone reviews the article, and then either accepts or rejects it. Explaining all that is a pain in the arse. Here is the expression of this workflow using the API:
89c8b37 @ryan-allen more readme updates
authored
30
09b60e3 @ryan-allen couple of tests that verify the API against the README... :) we're getti...
authored
31 Workflow.specify 'Article Workflow' do
89c8b37 @ryan-allen more readme updates
authored
32 state :new do
33 event :submit, :transitions_to => :awaiting_review
34 end
35 state :awaiting_review do
36 event :review, :transitions_to => :being_reviewed
37 end
38 state :being_reviewed do
39 event :accept, :transitions_to => :accepted
b07506e @ryan-allen moar updates on state machine
authored
40 event :reject, :transitions_to => :rejected
89c8b37 @ryan-allen more readme updates
authored
41 end
42 state :accepted
43 state :rejected
44 end
d2b0944 @ryan-allen moar readme updates
authored
45
46 Much better, isn't it!
89c8b37 @ryan-allen more readme updates
authored
47
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
48 The initial state is <tt>:new</tt> – in this example that's somewhat meaningless. (?) However, the <tt>:submit</tt> event <tt>:transitions_to => :being_reviewed</tt>. So, lets instantiate an instance of this Workflow:
89c8b37 @ryan-allen more readme updates
authored
49
50 workflow = Workflow.new('Article Workflow')
51 workflow.state # => :new
52
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
53 Now we can call the submit event, which transitions to the <tt>:awaiting_review</tt> state:
89c8b37 @ryan-allen more readme updates
authored
54
55 workflow.submit
437a51b @ryan-allen even moar documentation
authored
56 workflow.state # => :awaiting_review
89c8b37 @ryan-allen more readme updates
authored
57
b07506e @ryan-allen moar updates on state machine
authored
58 Events are actually instance methods on a workflow, and depending on the state you're in, you'll have a different set of events used to transition to other states.
89c8b37 @ryan-allen more readme updates
authored
59
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
60 Given this workflow is now <tt>:awaiting_approval</tt>, we have a <tt>:review</tt> event, that we call when someone begins to review the article, which puts the workflow into the <tt>:being_reviewed</tt> state.
b07506e @ryan-allen moar updates on state machine
authored
61
6d9cdfa @ryan-allen moar readme + todos
authored
62 States can also be queried via predicates for convenience like so:
63
64 workflow = Workflow.new('Article Workflow')
65 workflow.new? # => true
66 workflow.awaiting_review? # => false
67 workflow.submit
68 workflow.new? # => false
69 workflow.awaiting_review? # => true
70
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
71 Lets say that the business rule is that only one person can review an article at a time – having a state <tt>:being_reviewed</tt> allows for doing things like checking which articles are being reviewed, and being able to select from a pool of articles that are awaiting review, etc. (rewrite?)
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
72
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
73 Now lets say another business rule is that we need to keep track of who is currently reviewing what, how do we do this? We'll now introduce the concept of an action by rewriting our <tt>:review</tt> event.
b07506e @ryan-allen moar updates on state machine
authored
74
75 event :review, :transitions_to => :being_reviewed do |reviewer|
76 # store the reviewer somewhere for later
77 end
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
78
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
79 By using Ruby blocks we've now introduced extra code to be fired when an event is called. The block parameters are treated as method arguments on the event, so, given we have a reference to the reviewer, the event call becomes:
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
80
b07506e @ryan-allen moar updates on state machine
authored
81 # we gots a reviewer
82 workflow.reivew(reviewer)
83
84 OK, so how do we store the reviewer? What is the scope inside that block? Ah, we'll get to that in a bit. An instance of a workflow isn't as useful as a workflow bound to an instance of another class. We'll introduce you to plain old Class integration and ActiveRecord integration later in this document.
85
86 So we've covered events, states, transitions and actions (as Ruby blocks). Now we're going to go over some hooks you have access to in a workflow. These are on_exit, on_entry and on_transition.
87
d2b0944 @ryan-allen moar readme updates
authored
88 When states transition, they are entered into, and exited out of, we can hook into this and do fancy junk.
89
437a51b @ryan-allen even moar documentation
authored
90 state :being_reviewed do
91 event :accept, :transitions_to => :accepted
92 event :reject, :transitions_to => :rejected
93 on_exit do |new_state, triggering_event, *event_args|
94 # do something related to coming out of :being_reviewed
95 end
96 end
97
98 state :accepted do
99 on_entry do |prior_state, triggering_event, *event_args|
100 # do something relevant to coming in to :accepted
101 end
102 end
d2b0944 @ryan-allen moar readme updates
authored
103
104 Now why don't we just put this code into an action block? Well, you might not have only one event that transitions into a state, you may have multiple events that transition to a particular state, so by using the on_entry and on_exit hooks you're guaranteeing that a certain bit of code is executed, regardless what event fires the transition.
105
106 Billy Bob the Manager comes to you and says "I need to know EVERYTHING THAT HAPPENS EVERYWHERE AT ANY TIME FOR EVERYTHING". For whatever reasons you have to record the history of the entire workflow. That's easy using on_transition.
107
437a51b @ryan-allen even moar documentation
authored
108 on_transition do |from, to, triggering_event, *event_args|
109 # record everything, or something
110 end
d2b0944 @ryan-allen moar readme updates
authored
111
112 Workflow doesn't try to tell you how to store your log messages, (but we'd suggest using a *splat and storing that somewhere, and keep your log messages flexible).
113
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
114 Finite state machines have the concept of a guard. The idea is that if a certain set of arbitrary conditions are not fulfilled, it will halt the transition from one state to another. We haven't really figured out how to do this, and we don't like the idea of going <tt>:guard => Proc.new {}</tt>, coz that's a bit lame, so instead we have <tt>halt!</tt>
d2b0944 @ryan-allen moar readme updates
authored
115
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
116 The <tt>halt!</tt> method is the implementation of the guard concept. Let's take a look.
d2b0944 @ryan-allen moar readme updates
authored
117
437a51b @ryan-allen even moar documentation
authored
118 state :being_reviewed do
119 event :accept, :transitions_to => :accepted do
120 halt if true # does not transition to :accepted
121 end
122 end
123
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
124 Inline with how ActiveRecord does things, <tt>halt!</tt> also can be called via <tt>halt</tt>, which makes the event return false, so you can trap it with if workflow.event instead of using a rescue block. Using halt returns false.
d2b0944 @ryan-allen moar readme updates
authored
125
437a51b @ryan-allen even moar documentation
authored
126 # using halt
960fe0e @ryan-allen adding halted? method to docs
authored
127 workflow.state # => :being_reviewed
128 workflow.accept # => false
129 workflow.halted? # => true
130 workflow.state # => :being_reviewed
437a51b @ryan-allen even moar documentation
authored
131
132 # using halt!
133 workflow.state # => :being_reviewed
134 begin
135 workflow.accept
136 rescue Workflow::Halted => e
137 # we gots an exception
138 end
960fe0e @ryan-allen adding halted? method to docs
authored
139 workflow.halted? # => true
140 workflow.state # => :being_reviewed
d2b0944 @ryan-allen moar readme updates
authored
141
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
142 Furthermore, <tt>halt!</tt> and <tt>halt</tt> accept an argument, which is the message why the workflow was halted.
d2b0944 @ryan-allen moar readme updates
authored
143
437a51b @ryan-allen even moar documentation
authored
144 state :being_reviewed do
145 event :accept, :transitions_to => :accepted do
146 halt 'coz I said so!' if true # does not transition to :accepted
147 end
148 end
149
c6ec064 @robolson Use <tt> tags instead of + signs in README
robolson authored
150 And the API for, like, getting this message, with both <tt>halt</tt> and <tt>halt!</tt>:
437a51b @ryan-allen even moar documentation
authored
151
152 # using halt
153 workflow.state # => :being_reviewed
154 workflow.accept # => false
960fe0e @ryan-allen adding halted? method to docs
authored
155 workflow.halted? # => true
437a51b @ryan-allen even moar documentation
authored
156 workflow.halted_because # => 'coz I said so!'
157 workflow.state # => :being_reviewed
158
159 # using halt!
160 workflow.state # => :being_reviewed
161 begin
162 workflow.accept
163 rescue Workflow::Halted => e
164 e.halted_because # => 'coz I said so!'
165 end
960fe0e @ryan-allen adding halted? method to docs
authored
166 workflow.halted? # => true
167 workflow.state # => :being_reviewed
d2b0944 @ryan-allen moar readme updates
authored
168
169 We can reflect off the workflow to (attempt) to automate as much as we can. There are two types of reflection in Workflow - reflection and meta-reflection. We'll explain the former first.
170
437a51b @ryan-allen even moar documentation
authored
171 workflow.states # => [:new, :awaiting_review, :being_reviewed, :accepted, :rejected]
172 workflow.states(:new).events # => [:submit]
173 workflow.states(:being_reviewed).events # => [:accept, :reject]
174 workflow.states(:being_reviewed).events(:accept).transitions_to # => :accepted
175
176 Meta-reflection allows you to add further information to your states, events in order to allow you to build whatever interface/controller/etc you require for your application. If reflection were Batman then meta-reflection is Robin, always there to lend a helping hand when Batman just isn't enough.
d2b0944 @ryan-allen moar readme updates
authored
177
437a51b @ryan-allen even moar documentation
authored
178 state :new, :meta => :ui_widget => :radio_buttons do
179 event :submit, :meta => :label => 'Upload...'
180 end
181
182 And as per the last example, getting yo meta is very similar:
183
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
184 workflow.states(:new).meta # => {:ui_widget => :radio_buttons}
437a51b @ryan-allen even moar documentation
authored
185 workflow.states(:new).meta[:ui_widget] # => :radio_buttons
186 workflow.states(:new).meta.ui_widget # => :radio_buttons
d2b0944 @ryan-allen moar readme updates
authored
187
437a51b @ryan-allen even moar documentation
authored
188 workflow.states(:new).events(:submit).meta # => {:label => 'Upload...'}
189 workflow.states(:new).events(:submit).meta[:label] # => 'Upload...'
190 workflow.states(:new).events(:submit).meta.label # => 'Upload...'
86e306d @ryan-allen even MOAR documentation
authored
191
192 Thankfully, meta responds to each so you can iterate over your values if you're so inclined.
193
194 workflow.states(:new).meta.each { |key, value| puts key, value }
437a51b @ryan-allen even moar documentation
authored
195
196 The order of which things are fired when an event are as follows:
197
198 * action
199 * on_transition (if action didn't halt)
200 * on_exit
201 * WORKFLOW STATE CHANGES, i.e. transition
202 * on_entry
203
204 Note that any event arguments are passed by reference, so if you modify action arguments in the action, or any of the hooks, it may affect hooked fired later.
d2b0944 @ryan-allen moar readme updates
authored
205
437a51b @ryan-allen even moar documentation
authored
206 We promised that we'd show you how to integrate workflow with your existing classes and instances, let look.
d2b0944 @ryan-allen moar readme updates
authored
207
86e306d @ryan-allen even MOAR documentation
authored
208 class Article
209 include Workflow
210 workflow do
211 state :new do
212 event :submit, :transitions_to => :awaiting_review
213 end
214 state :awaiting_review do
215 event :approve, :transitions_to => :approved
216 end
217 state :approved
218 # ...
219 end
220 end
d2b0944 @ryan-allen moar readme updates
authored
221
86e306d @ryan-allen even MOAR documentation
authored
222 article = Article.new
223 article.state # => :new
224 article.submit
225 article.state # => :awaiting_review
226 article.approve
227 article.state # => :approved
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
228
19a08ef @robolson ActiveRecord field is 'workflow_state' not 'state'
robolson authored
229 And as ActiveRecord is all the rage these days, all you need is a string field on the table called "workflow_state", which is used to store the current state. Workflow handles auto-setting of a state after a find, yet it doesn't save a record after a transition (though you could make it do this in on_transition).
86e306d @ryan-allen even MOAR documentation
authored
230
231 class Article < ActiveRecord::Base
232 include Workflow
233 workflow do
234 # ...
235 end
236 end
14b1461 @ryan-allen starting to write docs for statemachine (soon to be Workflow)
authored
237
6ce5bca @robolson Added code tags in appropriate places throughout the README
robolson authored
238 When integrating with other classes, behind the scenes, Workflow sets up a Proxy to method missing. A probable common error would be to call an event that doesn't exist, so we catch +NoMethodError+'s and helpfully let you know what available events exist:
86e306d @ryan-allen even MOAR documentation
authored
239
9cba632 @ryan-allen moar for README, we're done, just need to proof and rewrite in a consist...
authored
240 class Article
241 include Workflow
242 workflow do
243 state :new do
244 event :submit, :transitions_to => :awaiting_review
245 end
246 state :awaiting_review do
247 event :approve, :transitions_to => :approved
248 end
249 state :approved
250 # ...
251 end
252 end
253
254 article = Article.new
255 article.aaaa
256 NoMethodError: undefined method `aaaa' for #<Article:0xe4e8>, conversely, if you were looking to call an event for its workflow, you're in the :new state, and the available events are [:submit]
257
258 So just incase you screw something up (like I did while testing this library), it'll give you a useful message.
485908d @ryan-allen update to documentation and specs, so we can support some production req...
authored
259
260 You can blatter existing workflows, by simply opening them up again (similar to how Ruby works!).
261
262 Workflow.specify 'Blatter' do
263 state :opened do
264 event :close, :transitions_to => :closed
265 end
266 state :closed
267 end
268
269 workflow = Workflow.new('Blatter')
270 workflow.close
271 workflow.state # => :closed
272 workflow.open # => raises a (nice) NoMethodError exception!
273
274 Workflow.specify 'Blatter' do
275 state :closed do
276 event :open, :transitions_to => :opened
277 end
278 end
279
280 workflow.open
281 workflow.state # => :opened
282
283 Workflow.specify 'Blatter' do
284 state :open do
285 event :close, :transitions_to => :jammed # the door is now faulty :)
286 end
287 state :jammed
288 end
289
290 workflow.close
291 workflow.state # => :jammed
292
293 Why can we do this? Well, we needed it for our production app, so there.
9cba632 @ryan-allen moar for README, we're done, just need to proof and rewrite in a consist...
authored
294
86e306d @ryan-allen even MOAR documentation
authored
295 And that's about it. A update to the implementation may allow multiple workflows per instance of a class or ActiveRecord, but we haven't figured out if that's required or appropriate.
296
39155e0 @ryan-allen WORKFLOW HAS A MAILING LIST (so just, like, letting you all know) :)
authored
297 Ryan Allen, March 2008.
Something went wrong with that request. Please try again.