workflow 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- version: 0.3.0
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: 2009-08-10 00:00:00 +02:00
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.rdoc
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.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.