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 +7 -0
- data/README.markdown +433 -0
- data/Rakefile +26 -26
- data/VERSION +1 -0
- data/lib/workflow.rb +127 -31
- data/test/couchtiny_example.rb +46 -0
- data/test/main_test.rb +420 -0
- data/test/readme_example.rb +37 -0
- data/test/without_active_record_test.rb +54 -0
- data/workflow.rb +1 -0
- metadata +32 -14
- data/README.rdoc +0 -452
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'workflow'
|
2
|
+
class Article
|
3
|
+
include Workflow
|
4
|
+
workflow do
|
5
|
+
state :new do
|
6
|
+
event :submit, :transitions_to => :awaiting_review
|
7
|
+
end
|
8
|
+
state :awaiting_review do
|
9
|
+
event :review, :transitions_to => :being_reviewed
|
10
|
+
end
|
11
|
+
state :being_reviewed do
|
12
|
+
event :accept, :transitions_to => :accepted
|
13
|
+
event :reject, :transitions_to => :rejected
|
14
|
+
end
|
15
|
+
state :accepted
|
16
|
+
state :rejected
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
article = Article.new
|
21
|
+
article.accepted? # => false
|
22
|
+
article.new? # => true
|
23
|
+
article.submit!
|
24
|
+
article.review!
|
25
|
+
|
26
|
+
puts article.current_state # => being_reviewed
|
27
|
+
|
28
|
+
|
29
|
+
class Article
|
30
|
+
def reject
|
31
|
+
puts "send email to the author here explaining the reason for the rejection"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
article.reject! # will cause a state transition, would persist the new
|
36
|
+
# state (if inherited from ActiveRecord), and invoke the callback -
|
37
|
+
# send email to the author.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'workflow'
|
3
|
+
|
4
|
+
class WithoutWorkflowTest < Test::Unit::TestCase
|
5
|
+
class Article
|
6
|
+
include Workflow
|
7
|
+
workflow do
|
8
|
+
state :new do
|
9
|
+
event :submit, :transitions_to => :awaiting_review
|
10
|
+
end
|
11
|
+
state :awaiting_review do
|
12
|
+
event :review, :transitions_to => :being_reviewed
|
13
|
+
end
|
14
|
+
state :being_reviewed do
|
15
|
+
event :accept, :transitions_to => :accepted
|
16
|
+
event :reject, :transitions_to => :rejected
|
17
|
+
end
|
18
|
+
state :accepted
|
19
|
+
state :rejected
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_readme_example_article
|
24
|
+
article = Article.new
|
25
|
+
assert article.new?
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'better error message on transitions_to typo' do
|
29
|
+
assert_raise Workflow::WorkflowDefinitionError do
|
30
|
+
Class.new do
|
31
|
+
include Workflow
|
32
|
+
workflow do
|
33
|
+
state :new do
|
34
|
+
event :event1, :transitionnn => :next # missing transitions_to target
|
35
|
+
end
|
36
|
+
state :next
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'check transition_to alias' do
|
43
|
+
Class.new do
|
44
|
+
include Workflow
|
45
|
+
workflow do
|
46
|
+
state :new do
|
47
|
+
event :event1, :transition_to => :next
|
48
|
+
end
|
49
|
+
state :next
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
data/workflow.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/lib/*.rb"].each { |rb| require rb }
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: workflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 4
|
8
|
+
- 1
|
9
|
+
version: 0.4.1
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Vladimir Dobriakov
|
@@ -9,51 +14,64 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date:
|
17
|
+
date: 2010-06-23 00:00:00 +02:00
|
13
18
|
default_executable:
|
14
19
|
dependencies: []
|
15
20
|
|
16
|
-
description:
|
21
|
+
description: " Workflow is a finite-state-machine-inspired API for modeling and interacting\n with what we tend to refer to as 'workflow'.\n\n * nice DSL to describe your states, events and transitions\n * robust integration with ActiveRecord and non relational data stores\n * various hooks for single transitions, entering state etc.\n * convenient access to the workflow specification: list states, possible events\n for particular state\n"
|
17
22
|
email: vladimir@geekq.net
|
18
23
|
executables: []
|
19
24
|
|
20
25
|
extensions: []
|
21
26
|
|
22
|
-
extra_rdoc_files:
|
23
|
-
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README.markdown
|
24
29
|
files:
|
30
|
+
- .gitignore
|
25
31
|
- MIT-LICENSE
|
26
|
-
- README.
|
32
|
+
- README.markdown
|
27
33
|
- Rakefile
|
34
|
+
- VERSION
|
28
35
|
- lib/workflow.rb
|
36
|
+
- test/couchtiny_example.rb
|
37
|
+
- test/main_test.rb
|
38
|
+
- test/readme_example.rb
|
29
39
|
- test/test_helper.rb
|
40
|
+
- test/without_active_record_test.rb
|
41
|
+
- workflow.rb
|
30
42
|
has_rdoc: true
|
31
43
|
homepage: http://blog.geekQ.net/
|
32
44
|
licenses: []
|
33
45
|
|
34
46
|
post_install_message:
|
35
|
-
rdoc_options:
|
36
|
-
|
47
|
+
rdoc_options:
|
48
|
+
- --charset=UTF-8
|
37
49
|
require_paths:
|
38
50
|
- lib
|
39
51
|
required_ruby_version: !ruby/object:Gem::Requirement
|
40
52
|
requirements:
|
41
53
|
- - ">="
|
42
54
|
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
43
57
|
version: "0"
|
44
|
-
version:
|
45
58
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
59
|
requirements:
|
47
60
|
- - ">="
|
48
61
|
- !ruby/object:Gem::Version
|
62
|
+
segments:
|
63
|
+
- 0
|
49
64
|
version: "0"
|
50
|
-
version:
|
51
65
|
requirements: []
|
52
66
|
|
53
|
-
rubyforge_project:
|
54
|
-
rubygems_version: 1.3.
|
67
|
+
rubyforge_project: workflow
|
68
|
+
rubygems_version: 1.3.6
|
55
69
|
signing_key:
|
56
70
|
specification_version: 3
|
57
71
|
summary: A replacement for acts_as_state_machine.
|
58
|
-
test_files:
|
59
|
-
|
72
|
+
test_files:
|
73
|
+
- test/couchtiny_example.rb
|
74
|
+
- test/main_test.rb
|
75
|
+
- test/test_helper.rb
|
76
|
+
- test/without_active_record_test.rb
|
77
|
+
- test/readme_example.rb
|
data/README.rdoc
DELETED
@@ -1,452 +0,0 @@
|
|
1
|
-
= Motivation for the fork
|
2
|
-
|
3
|
-
Why to overflow the world with yet another fork of the workflow library?
|
4
|
-
|
5
|
-
Well, while the workflow definition API of the original library is nice,
|
6
|
-
the implementation of the ActiveRecord integration and the remaining API
|
7
|
-
are very problematic.
|
8
|
-
|
9
|
-
== API improvements
|
10
|
-
|
11
|
-
* Fixed fuzzy API. For example, the states() function returned a state object
|
12
|
-
or an array of strings, depending on function parameters!
|
13
|
-
While somebody could find the usage like `states(states.first)` funny,
|
14
|
-
it leads to a maintanence nightmare and the usage is difficult to explain
|
15
|
-
to the new team members.
|
16
|
-
|
17
|
-
* When we activate the state transition, we typically do not make any other
|
18
|
-
changes to the attributes of the entity. So by default the event
|
19
|
-
invokation now immediately saves the new state to the database.
|
20
|
-
`update_attribute` is used for implementation. This can be overriden
|
21
|
-
with the `persist_workflow_state` method.
|
22
|
-
|
23
|
-
* We've noticed, that mixing the list of events and states with the blocks
|
24
|
-
invoked for particular transitions leads to a bumpy and poorly readable code
|
25
|
-
due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually
|
26
|
-
we desided to invoke an optional user defined callback method with the same
|
27
|
-
name as the event (convention over configuration)
|
28
|
-
|
29
|
-
event :my_transition, :transitions_to => other_state
|
30
|
-
|
31
|
-
defines a transition method `my_transition!` and invokes user defined callback
|
32
|
-
function `my_transition` (with no exclamation mark). The old way - using
|
33
|
-
a block or a combination of both is also possible.
|
34
|
-
|
35
|
-
* In the API the workflow specification is clearly separated from the object
|
36
|
-
state. The former is accessible through the class method
|
37
|
-
`MyEntityClass.workflow_spec` (including all the meta data) and the latter
|
38
|
-
is integrated in the instance of the my_entity, e.g. my_entity.current_state
|
39
|
-
or my_entity.my_event!
|
40
|
-
|
41
|
-
|
42
|
-
== Implementation improvements
|
43
|
-
|
44
|
-
* Replaced the extensive usage of method_missing with a simple generation
|
45
|
-
of needed functions like `my_state?` and `my_event`. Advantages:
|
46
|
-
|
47
|
-
* shorter and more meaningful stack during debugging
|
48
|
-
* public_methods shows the methods
|
49
|
-
* autocompletion in irb works
|
50
|
-
|
51
|
-
* Do not use ActiveRecord hooks leading to the divergence of `workflow_state`
|
52
|
-
table attribute and wokflow.current_state.
|
53
|
-
|
54
|
-
* Fixed all the warnings and usage of obsolete API.
|
55
|
-
|
56
|
-
* The messy and fuzzy API is probably partially caused by RSpec driven
|
57
|
-
development. While RSpec can be useful for gathering and mapping business
|
58
|
-
requirements, it is IMHO totally unsuitable for driving a clean, explicit,
|
59
|
-
orthogonal API. If I say `state` in the natural language or in a RSpec, it is
|
60
|
-
not clear, what I mean - a string, a symbol, an object of class State, a hash?
|
61
|
-
This(?) led to this fuzzy API.
|
62
|
-
So I switched to the plain old unit tests.
|
63
|
-
|
64
|
-
* Eliminated bidirectional connection between the model
|
65
|
-
class and Workflow::Instance - the bind_to, @context, @workflow.
|
66
|
-
|
67
|
-
* Only one file with 240 lines of code and no interdependencies between classes.
|
68
|
-
As little meta programming as needed - not as much as possible. ;-)
|
69
|
-
So you can easily extend or modify it to suit your needs.
|
70
|
-
|
71
|
-
|
72
|
-
== Installation
|
73
|
-
|
74
|
-
gem install workflow
|
75
|
-
|
76
|
-
Alternatively you can just download the lib/workflow.rb and put it in the lib folder of your
|
77
|
-
Rails application.
|
78
|
-
|
79
|
-
|
80
|
-
== Migration from the original Ryan's library
|
81
|
-
|
82
|
-
Credit: Michael (rockrep)
|
83
|
-
|
84
|
-
* Accessing workflow specification
|
85
|
-
my_instance.workflow # old
|
86
|
-
MyClass.workflow_spec # new
|
87
|
-
|
88
|
-
* Accessing states, events, meta, e.g.
|
89
|
-
my_instance.workflow.states(:some_state).events(:some_event).meta[:some_meta_tag] # old
|
90
|
-
MyClass.workflow_spec.states[:some_state].events[:some_event].meta[:some_meta_tag] # new
|
91
|
-
|
92
|
-
* Causing state transitions
|
93
|
-
my_instance.workflow.my_event # old
|
94
|
-
my_instance.my_event! # new
|
95
|
-
|
96
|
-
* when using both a block and a callback method for an event, the block executes prior to the callback
|
97
|
-
|
98
|
-
|
99
|
-
== About
|
100
|
-
|
101
|
-
Author: Vladimir Dobriakov, http://www.innoq.com/blog/vd, http://blog.geekq.net/
|
102
|
-
|
103
|
-
Copyright (c) 2008-2009 Vodafone
|
104
|
-
|
105
|
-
Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
|
106
|
-
|
107
|
-
Based on the work of Ryan Allen and Scott Barron
|
108
|
-
|
109
|
-
Licensed under MIT license, see the MIT-LICENSE file.
|
110
|
-
|
111
|
-
|
112
|
-
== New in the version 0.3.0
|
113
|
-
|
114
|
-
Intermixing of transition graph definition (states, transitions)
|
115
|
-
on the one side and implementation of the actions on the other side
|
116
|
-
for a bigger state machine can introduce clutter.
|
117
|
-
|
118
|
-
To reduce this clutter it is now possible to use state entry- and
|
119
|
-
exit- hooks defined through a naming convention. For example, if there
|
120
|
-
is a state :pending, then instead of using a
|
121
|
-
block:
|
122
|
-
|
123
|
-
state :pending do
|
124
|
-
on_entry do
|
125
|
-
# your implementation here
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
you can hook in by defining method
|
130
|
-
|
131
|
-
def on_pending_exit(new_state, event, *args)
|
132
|
-
# your implementation here
|
133
|
-
end
|
134
|
-
|
135
|
-
anywhere in your class. You can also use a simpler function signature
|
136
|
-
like `def on_pending_exit(*args)` if your are not interested in arguments -
|
137
|
-
`def on_pending_exit()` with an empty list would not work.
|
138
|
-
|
139
|
-
If both a function with a name according to naming convention and the
|
140
|
-
on_entry/on_exit block are given, then only on_entry/on_exit block is used.
|
141
|
-
|
142
|
-
|
143
|
-
= Original readme
|
144
|
-
|
145
|
-
Disclaimer: my fork is not 100% API compatible to the original library by Ryan.
|
146
|
-
I'll update/merge the readme as soon as posssible.
|
147
|
-
In the mean time please use the original readme in conjunction with
|
148
|
-
the API changes and migration hints listed above.
|
149
|
-
|
150
|
-
|
151
|
-
=== New Mailing List!
|
152
|
-
|
153
|
-
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!!!
|
154
|
-
|
155
|
-
http://groups.google.com/group/ruby-workflow
|
156
|
-
|
157
|
-
See you there!
|
158
|
-
|
159
|
-
=== What is workflow?
|
160
|
-
|
161
|
-
Workflow is a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as 'workflow'.
|
162
|
-
|
163
|
-
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.
|
164
|
-
|
165
|
-
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.
|
166
|
-
|
167
|
-
Now, all that's a mouthful, but we'll demonstrate the API bit by bit with a real-ish world example.
|
168
|
-
|
169
|
-
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:
|
170
|
-
|
171
|
-
|
172
|
-
class Article
|
173
|
-
include Workflow
|
174
|
-
workflow do
|
175
|
-
state :new do
|
176
|
-
event :submit, :transitions_to => :awaiting_review
|
177
|
-
end
|
178
|
-
state :awaiting_review do
|
179
|
-
event :review, :transitions_to => :being_reviewed
|
180
|
-
end
|
181
|
-
state :being_reviewed do
|
182
|
-
event :accept, :transitions_to => :accepted
|
183
|
-
event :reject, :transitions_to => :rejected
|
184
|
-
end
|
185
|
-
state :accepted
|
186
|
-
state :rejected
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
Much better, isn't it!
|
191
|
-
|
192
|
-
Let's create an article instance and check in which state it is:
|
193
|
-
|
194
|
-
article = Article.new
|
195
|
-
article.accepted? # => false
|
196
|
-
article.new? # => true
|
197
|
-
|
198
|
-
You can also access the whole +current_state+ object including the list of possible events and other meta information:
|
199
|
-
|
200
|
-
article.current_state
|
201
|
-
=> #<Workflow::State:0x7f1e3d6731f0 @events={
|
202
|
-
:submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
|
203
|
-
@transitions_to=:awaiting_review, @name=:submit, @meta={}>},
|
204
|
-
name:new, meta{}
|
205
|
-
|
206
|
-
Now we can call the submit event, which transitions to the <tt>:awaiting_review</tt> state:
|
207
|
-
|
208
|
-
article.submit!
|
209
|
-
article.awaiting_review? # => true
|
210
|
-
|
211
|
-
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.
|
212
|
-
|
213
|
-
TODO - continue editing
|
214
|
-
|
215
|
-
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.
|
216
|
-
|
217
|
-
States can also be queried via predicates for convenience like so:
|
218
|
-
|
219
|
-
workflow = Workflow.new('Article Workflow')
|
220
|
-
workflow.new? # => true
|
221
|
-
workflow.awaiting_review? # => false
|
222
|
-
workflow.submit
|
223
|
-
workflow.new? # => false
|
224
|
-
workflow.awaiting_review? # => true
|
225
|
-
|
226
|
-
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?)
|
227
|
-
|
228
|
-
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.
|
229
|
-
|
230
|
-
event :review, :transitions_to => :being_reviewed do |reviewer|
|
231
|
-
# store the reviewer somewhere for later
|
232
|
-
end
|
233
|
-
|
234
|
-
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:
|
235
|
-
|
236
|
-
# we gots a reviewer
|
237
|
-
workflow.reivew(reviewer)
|
238
|
-
|
239
|
-
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.
|
240
|
-
|
241
|
-
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.
|
242
|
-
|
243
|
-
When states transition, they are entered into, and exited out of, we can hook into this and do fancy junk.
|
244
|
-
|
245
|
-
state :being_reviewed do
|
246
|
-
event :accept, :transitions_to => :accepted
|
247
|
-
event :reject, :transitions_to => :rejected
|
248
|
-
on_exit do |new_state, triggering_event, *event_args|
|
249
|
-
# do something related to coming out of :being_reviewed
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
state :accepted do
|
254
|
-
on_entry do |prior_state, triggering_event, *event_args|
|
255
|
-
# do something relevant to coming in to :accepted
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
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.
|
260
|
-
|
261
|
-
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.
|
262
|
-
|
263
|
-
on_transition do |from, to, triggering_event, *event_args|
|
264
|
-
# record everything, or something
|
265
|
-
end
|
266
|
-
|
267
|
-
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).
|
268
|
-
|
269
|
-
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>
|
270
|
-
|
271
|
-
The <tt>halt!</tt> method is the implementation of the guard concept. Let's take a look.
|
272
|
-
|
273
|
-
state :being_reviewed do
|
274
|
-
event :accept, :transitions_to => :accepted do
|
275
|
-
halt if true # does not transition to :accepted
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
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.
|
280
|
-
|
281
|
-
# using halt
|
282
|
-
workflow.state # => :being_reviewed
|
283
|
-
workflow.accept # => false
|
284
|
-
workflow.halted? # => true
|
285
|
-
workflow.state # => :being_reviewed
|
286
|
-
|
287
|
-
# using halt!
|
288
|
-
workflow.state # => :being_reviewed
|
289
|
-
begin
|
290
|
-
workflow.accept
|
291
|
-
rescue Workflow::Halted => e
|
292
|
-
# we gots an exception
|
293
|
-
end
|
294
|
-
workflow.halted? # => true
|
295
|
-
workflow.state # => :being_reviewed
|
296
|
-
|
297
|
-
Furthermore, <tt>halt!</tt> and <tt>halt</tt> accept an argument, which is the message why the workflow was halted.
|
298
|
-
|
299
|
-
state :being_reviewed do
|
300
|
-
event :accept, :transitions_to => :accepted do
|
301
|
-
halt 'coz I said so!' if true # does not transition to :accepted
|
302
|
-
end
|
303
|
-
end
|
304
|
-
|
305
|
-
And the API for, like, getting this message, with both <tt>halt</tt> and <tt>halt!</tt>:
|
306
|
-
|
307
|
-
# using halt
|
308
|
-
workflow.state # => :being_reviewed
|
309
|
-
workflow.accept # => false
|
310
|
-
workflow.halted? # => true
|
311
|
-
workflow.halted_because # => 'coz I said so!'
|
312
|
-
workflow.state # => :being_reviewed
|
313
|
-
|
314
|
-
# using halt!
|
315
|
-
workflow.state # => :being_reviewed
|
316
|
-
begin
|
317
|
-
workflow.accept
|
318
|
-
rescue Workflow::Halted => e
|
319
|
-
e.halted_because # => 'coz I said so!'
|
320
|
-
end
|
321
|
-
workflow.halted? # => true
|
322
|
-
workflow.state # => :being_reviewed
|
323
|
-
|
324
|
-
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.
|
325
|
-
|
326
|
-
workflow.states # => [:new, :awaiting_review, :being_reviewed, :accepted, :rejected]
|
327
|
-
workflow.states(:new).events # => [:submit]
|
328
|
-
workflow.states(:being_reviewed).events # => [:accept, :reject]
|
329
|
-
workflow.states(:being_reviewed).events(:accept).transitions_to # => :accepted
|
330
|
-
|
331
|
-
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.
|
332
|
-
|
333
|
-
state :new, :meta => :ui_widget => :radio_buttons do
|
334
|
-
event :submit, :meta => :label => 'Upload...'
|
335
|
-
end
|
336
|
-
|
337
|
-
And as per the last example, getting yo meta is very similar:
|
338
|
-
|
339
|
-
workflow.states(:new).meta # => {:ui_widget => :radio_buttons}
|
340
|
-
workflow.states(:new).meta[:ui_widget] # => :radio_buttons
|
341
|
-
workflow.states(:new).meta.ui_widget # => :radio_buttons
|
342
|
-
|
343
|
-
workflow.states(:new).events(:submit).meta # => {:label => 'Upload...'}
|
344
|
-
workflow.states(:new).events(:submit).meta[:label] # => 'Upload...'
|
345
|
-
workflow.states(:new).events(:submit).meta.label # => 'Upload...'
|
346
|
-
|
347
|
-
Thankfully, meta responds to each so you can iterate over your values if you're so inclined.
|
348
|
-
|
349
|
-
workflow.states(:new).meta.each { |key, value| puts key, value }
|
350
|
-
|
351
|
-
The order of which things are fired when an event are as follows:
|
352
|
-
|
353
|
-
* action
|
354
|
-
* on_transition (if action didn't halt)
|
355
|
-
* on_exit
|
356
|
-
* WORKFLOW STATE CHANGES, i.e. transition
|
357
|
-
* on_entry
|
358
|
-
|
359
|
-
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.
|
360
|
-
|
361
|
-
We promised that we'd show you how to integrate workflow with your existing classes and instances, let look.
|
362
|
-
|
363
|
-
class Article
|
364
|
-
include Workflow
|
365
|
-
workflow do
|
366
|
-
state :new do
|
367
|
-
event :submit, :transitions_to => :awaiting_review
|
368
|
-
end
|
369
|
-
state :awaiting_review do
|
370
|
-
event :approve, :transitions_to => :approved
|
371
|
-
end
|
372
|
-
state :approved
|
373
|
-
# ...
|
374
|
-
end
|
375
|
-
end
|
376
|
-
|
377
|
-
article = Article.new
|
378
|
-
article.state # => :new
|
379
|
-
article.submit
|
380
|
-
article.state # => :awaiting_review
|
381
|
-
article.approve
|
382
|
-
article.state # => :approved
|
383
|
-
|
384
|
-
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).
|
385
|
-
|
386
|
-
class Article < ActiveRecord::Base
|
387
|
-
include Workflow
|
388
|
-
workflow do
|
389
|
-
# ...
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
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:
|
394
|
-
|
395
|
-
class Article
|
396
|
-
include Workflow
|
397
|
-
workflow do
|
398
|
-
state :new do
|
399
|
-
event :submit, :transitions_to => :awaiting_review
|
400
|
-
end
|
401
|
-
state :awaiting_review do
|
402
|
-
event :approve, :transitions_to => :approved
|
403
|
-
end
|
404
|
-
state :approved
|
405
|
-
# ...
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
article = Article.new
|
410
|
-
article.aaaa
|
411
|
-
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]
|
412
|
-
|
413
|
-
So just incase you screw something up (like I did while testing this library), it'll give you a useful message.
|
414
|
-
|
415
|
-
You can blatter existing workflows, by simply opening them up again (similar to how Ruby works!).
|
416
|
-
|
417
|
-
Workflow.specify 'Blatter' do
|
418
|
-
state :opened do
|
419
|
-
event :close, :transitions_to => :closed
|
420
|
-
end
|
421
|
-
state :closed
|
422
|
-
end
|
423
|
-
|
424
|
-
workflow = Workflow.new('Blatter')
|
425
|
-
workflow.close
|
426
|
-
workflow.state # => :closed
|
427
|
-
workflow.open # => raises a (nice) NoMethodError exception!
|
428
|
-
|
429
|
-
Workflow.specify 'Blatter' do
|
430
|
-
state :closed do
|
431
|
-
event :open, :transitions_to => :opened
|
432
|
-
end
|
433
|
-
end
|
434
|
-
|
435
|
-
workflow.open
|
436
|
-
workflow.state # => :opened
|
437
|
-
|
438
|
-
Workflow.specify 'Blatter' do
|
439
|
-
state :open do
|
440
|
-
event :close, :transitions_to => :jammed # the door is now faulty :)
|
441
|
-
end
|
442
|
-
state :jammed
|
443
|
-
end
|
444
|
-
|
445
|
-
workflow.close
|
446
|
-
workflow.state # => :jammed
|
447
|
-
|
448
|
-
Why can we do this? Well, we needed it for our production app, so there.
|
449
|
-
|
450
|
-
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.
|
451
|
-
|
452
|
-
Ryan Allen, March 2008.
|