workflow 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
+ Copyright (c) 2008-2009 Vodafone
1
2
  Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -16,4 +17,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
17
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
18
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
19
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- THE SOFTWARE.
20
+ THE SOFTWARE.
data/README.rdoc CHANGED
@@ -71,23 +71,83 @@ are very problematic.
71
71
 
72
72
  == Installation
73
73
 
74
- You can just download the lib/workflow.rb and put it in the lib folder of your
74
+ gem install workflow
75
+
76
+ Alternatively you can just download the lib/workflow.rb and put it in the lib folder of your
75
77
  Rails application.
76
78
 
77
- Later I'll probable create a gem.
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
78
97
 
79
98
 
80
99
  == About
81
100
 
82
101
  Author: Vladimir Dobriakov, http://www.innoq.com/blog/vd, http://blog.geekq.net/
83
102
 
84
- Parts copyright 2009 Vodafone
103
+ Copyright (c) 2008-2009 Vodafone
104
+
105
+ Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
85
106
 
86
107
  Based on the work of Ryan Allen and Scott Barron
87
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
+
88
142
 
89
143
  = Original readme
90
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
+
91
151
  === New Mailing List!
92
152
 
93
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!!!
@@ -108,35 +168,50 @@ Now, all that's a mouthful, but we'll demonstrate the API bit by bit with a real
108
168
 
109
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:
110
170
 
111
- Workflow.specify 'Article Workflow' do
112
- state :new do
113
- event :submit, :transitions_to => :awaiting_review
114
- end
115
- state :awaiting_review do
116
- event :review, :transitions_to => :being_reviewed
117
- end
118
- state :being_reviewed do
119
- event :accept, :transitions_to => :accepted
120
- event :reject, :transitions_to => :rejected
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
121
187
  end
122
- state :accepted
123
- state :rejected
124
188
  end
125
-
189
+
126
190
  Much better, isn't it!
127
191
 
128
- 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:
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{}
129
205
 
130
- workflow = Workflow.new('Article Workflow')
131
- workflow.state # => :new
132
-
133
206
  Now we can call the submit event, which transitions to the <tt>:awaiting_review</tt> state:
134
207
 
135
- workflow.submit
136
- workflow.state # => :awaiting_review
208
+ article.submit!
209
+ article.awaiting_review? # => true
137
210
 
138
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.
139
212
 
213
+ TODO - continue editing
214
+
140
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.
141
216
 
142
217
  States can also be queried via predicates for convenience like so:
data/Rakefile CHANGED
@@ -8,11 +8,12 @@ task :default => [:test]
8
8
  Rake::TestTask.new do |t|
9
9
  t.verbose = true
10
10
  t.warning = true
11
+ t.pattern = 'test/*_test.rb'
11
12
  end
12
13
 
13
- PKG_VERSION = "0.2.0"
14
+ PKG_VERSION = "0.3.0"
14
15
  PKG_FILES = FileList[
15
- 'LICENSE',
16
+ 'MIT-LICENSE',
16
17
  'README.rdoc',
17
18
  'Rakefile',
18
19
  'lib/**/*.rb',
@@ -33,7 +34,7 @@ end
33
34
 
34
35
  Rake::RDocTask.new do |rdoc|
35
36
  rdoc.main = "README"
36
- rdoc.rdoc_files.include("README", "lib/**/*.rb")
37
+ rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
37
38
  rdoc.options << "-S"
38
39
  end
39
40
 
data/lib/workflow.rb CHANGED
@@ -55,6 +55,8 @@ module Workflow
55
55
 
56
56
  class NoTransitionAllowed < Exception; end
57
57
 
58
+ class WorkflowError < Exception; end
59
+
58
60
  class State
59
61
 
60
62
  attr_accessor :name, :events, :meta, :on_entry, :on_exit
@@ -138,6 +140,7 @@ module Workflow
138
140
  false
139
141
  end
140
142
  else
143
+ check_transition(event)
141
144
  run_on_transition(current_state, spec.states[event.transitions_to], name, *args)
142
145
  transition(current_state, spec.states[event.transitions_to], name, *args)
143
146
  return_value
@@ -146,6 +149,16 @@ module Workflow
146
149
 
147
150
  private
148
151
 
152
+ def check_transition(event)
153
+ # Create a meaningful error message instead of
154
+ # "undefined method `on_entry' for nil:NilClass"
155
+ # Reported by Kyle Burton
156
+ if !spec.states[event.transitions_to]
157
+ raise WorkflowError.new("Event[#{event.name}]'s " +
158
+ "transitions_to[#{event.transitions_to}] is not a declared state.")
159
+ end
160
+ end
161
+
149
162
  def spec
150
163
  c = self.class
151
164
  # using a simple loop instead of class_inheritable_accessor to avoid
@@ -187,11 +200,23 @@ module Workflow
187
200
  end
188
201
 
189
202
  def run_on_entry(state, prior_state, triggering_event, *args)
190
- instance_exec(prior_state.name, triggering_event, *args, &state.on_entry) if state.on_entry
203
+ if state.on_entry
204
+ instance_exec(prior_state.name, triggering_event, *args, &state.on_entry)
205
+ else
206
+ hook_name = "on_#{state}_entry"
207
+ self.send hook_name, prior_state, triggering_event, *args if self.respond_to? hook_name
208
+ end
191
209
  end
192
210
 
193
211
  def run_on_exit(state, new_state, triggering_event, *args)
194
- instance_exec(new_state.name, triggering_event, *args, &state.on_exit) if state and state.on_exit
212
+ if state
213
+ if state.on_exit
214
+ instance_exec(new_state.name, triggering_event, *args, &state.on_exit)
215
+ else
216
+ hook_name = "on_#{state}_exit"
217
+ self.send hook_name, new_state, triggering_event, *args if self.respond_to? hook_name
218
+ end
219
+ end
195
220
  end
196
221
 
197
222
  # load_workflow_state and persist_workflow_state
@@ -236,9 +261,11 @@ module Workflow
236
261
  def self.included(klass)
237
262
  klass.send :include, WorkflowInstanceMethods
238
263
  klass.extend WorkflowClassMethods
239
- if klass < ActiveRecord::Base
264
+ if Object.const_defined?(:ActiveRecord)
265
+ if klass < ActiveRecord::Base
240
266
  klass.send :include, ActiveRecordInstanceMethods
241
267
  klass.before_validation :write_initial_state
268
+ end
242
269
  end
243
270
  end
244
271
  end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ class << Test::Unit::TestCase
5
+ def test(name, &block)
6
+ test_name = :"test_#{name.gsub(' ','_')}"
7
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
8
+ if block
9
+ define_method test_name, &block
10
+ else
11
+ puts "PENDING: #{name}"
12
+ end
13
+ end
14
+ end
15
+
16
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dobriakov
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-29 00:00:00 +02:00
12
+ date: 2009-08-10 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -22,12 +22,12 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
 
24
24
  files:
25
- - LICENSE
25
+ - MIT-LICENSE
26
26
  - README.rdoc
27
27
  - Rakefile
28
28
  - lib/workflow.rb
29
- - test/test_workflow.rb
30
- has_rdoc: false
29
+ - test/test_helper.rb
30
+ has_rdoc: true
31
31
  homepage: http://blog.geekQ.net/
32
32
  licenses: []
33
33
 
@@ -51,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements: []
52
52
 
53
53
  rubyforge_project:
54
- rubygems_version: 1.3.1.2403
54
+ rubygems_version: 1.3.3
55
55
  signing_key:
56
56
  specification_version: 3
57
57
  summary: A replacement for acts_as_state_machine.
@@ -1,252 +0,0 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- old_verbose, $VERBOSE = $VERBOSE, nil
4
- require 'active_record'
5
- require 'sqlite3'
6
- $VERBOSE = old_verbose
7
- require 'workflow'
8
- require 'mocha'
9
- #require 'ruby-debug'
10
-
11
- ActiveRecord::Migration.verbose = false
12
-
13
- class << Test::Unit::TestCase
14
- def test(name, &block)
15
- test_name = :"test_#{name.gsub(' ','_')}"
16
- raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name.to_s
17
- if block
18
- define_method test_name, &block
19
- else
20
- puts "PENDING: #{name}"
21
- end
22
- end
23
- end
24
-
25
- class Order < ActiveRecord::Base
26
- include Workflow
27
- workflow do
28
- state :submitted do
29
- event :accept, :transitions_to => :accepted, :meta => {:doc_weight => 8} do |reviewer, args|
30
- end
31
- end
32
- state :accepted do
33
- event :ship, :transitions_to => :shipped
34
- end
35
- state :shipped
36
- end
37
-
38
- end
39
-
40
- class WorkflowTest < Test::Unit::TestCase
41
-
42
- def exec(sql)
43
- ActiveRecord::Base.connection.execute sql
44
- end
45
-
46
- def setup
47
- old_verbose, $VERBOSE = $VERBOSE, nil # eliminate sqlite3 warning. TODO: delete as soon as sqlite-ruby is fixed
48
- ActiveRecord::Base.establish_connection(
49
- :adapter => "sqlite3",
50
- :database => ":memory:" #"tmp/test"
51
- )
52
- ActiveRecord::Base.connection.reconnect! # eliminate ActiveRecord warning. TODO: delete as soon as ActiveRecord is fixed
53
-
54
- ActiveRecord::Schema.define do
55
- create_table :orders do |t|
56
- t.string :title, :null => false
57
- t.string :workflow_state
58
- end
59
- end
60
-
61
- exec "INSERT INTO orders(title, workflow_state) VALUES('some order', 'accepted')"
62
- $VERBOSE = old_verbose
63
- end
64
-
65
- def teardown
66
- ActiveRecord::Base.connection.disconnect!
67
- end
68
-
69
- def assert_state(title, expected_state)
70
- o = Order.find_by_title(title)
71
- assert_equal expected_state, o.read_attribute(:workflow_state)
72
- o
73
- end
74
-
75
- test 'immediatly save the new workflow_state on state machine transition' do
76
- o = assert_state 'some order', 'accepted'
77
- o.ship!
78
- assert_state 'some order', 'shipped'
79
- end
80
-
81
- test 'persist workflow_state in the db and reload' do
82
- o = assert_state 'some order', 'accepted'
83
- assert_equal :accepted, o.current_state.name
84
- o.ship!
85
- o.save!
86
-
87
- assert_state 'some order', 'shipped'
88
-
89
- o.reload
90
- assert_equal 'shipped', o.read_attribute(:workflow_state)
91
- end
92
-
93
- test 'access workflow specification' do
94
- assert_equal 3, Order.workflow_spec.states.length
95
- end
96
-
97
- test 'current state object' do
98
- o = assert_state 'some order', 'accepted'
99
- assert_equal 'accepted', o.current_state.to_s
100
- assert_equal 1, o.current_state.events.length
101
- end
102
-
103
- test 'on_entry and on_exit invoked' do
104
- c = Class.new
105
- callbacks = mock()
106
- callbacks.expects(:my_on_exit_new).once
107
- callbacks.expects(:my_on_entry_old).once
108
- c.class_eval do
109
- include Workflow
110
- workflow do
111
- state :new do
112
- event :age, :transitions_to => :old
113
- end
114
- on_exit do
115
- callbacks.my_on_exit_new
116
- end
117
- state :old
118
- on_entry do
119
- callbacks.my_on_entry_old
120
- end
121
- on_exit do
122
- fail "wrong on_exit executed"
123
- end
124
- end
125
- end
126
-
127
- o = c.new
128
- assert_equal 'new', o.current_state.to_s
129
- o.age!
130
- end
131
-
132
- test 'on_transition invoked' do
133
- callbacks = mock()
134
- callbacks.expects(:on_tran).once # this is validated at the end
135
- c = Class.new
136
- c.class_eval do
137
- include Workflow
138
- workflow do
139
- state :one do
140
- event :increment, :transitions_to => :two
141
- end
142
- state :two
143
- on_transition do |from, to, triggering_event, *event_args|
144
- callbacks.on_tran
145
- end
146
- end
147
- end
148
- assert_not_nil c.workflow_spec.on_transition_proc
149
- c.new.increment!
150
- end
151
-
152
- test 'access event meta information' do
153
- c = Class.new
154
- c.class_eval do
155
- include Workflow
156
- workflow do
157
- state :main, :meta => {:importance => 8}
158
- state :supplemental, :meta => {:importance => 1}
159
- end
160
- end
161
- assert_equal 1, c.workflow_spec.states[:supplemental].meta[:importance]
162
- end
163
-
164
- test 'initial state' do
165
- c = Class.new
166
- c.class_eval do
167
- include Workflow
168
- workflow { state :one; state :two }
169
- end
170
- assert_equal 'one', c.new.current_state.to_s
171
- end
172
-
173
- test 'nil as initial state' do
174
- exec "INSERT INTO orders(title, workflow_state) VALUES('nil state', NULL)"
175
- o = Order.find_by_title('nil state')
176
- assert o.submitted?, 'if workflow_state is nil, the initial state should be assumed'
177
- assert !o.shipped?
178
- end
179
-
180
- test 'initial state immediately set as ActiveRecord attribute for new objects' do
181
- o = Order.create(:title => 'new object')
182
- assert_equal 'submitted', o.read_attribute(:workflow_state)
183
- end
184
-
185
- test 'question methods for state' do
186
- o = assert_state 'some order', 'accepted'
187
- assert o.accepted?
188
- assert !o.shipped?
189
- end
190
-
191
- test 'correct exception for event, that is not allowed in current state' do
192
- o = assert_state 'some order', 'accepted'
193
- assert_raise Workflow::NoTransitionAllowed do
194
- o.accept!
195
- end
196
- end
197
-
198
- test 'multiple events with the same name and different arguments lists from different states'
199
-
200
- test 'implicit transition callback' do
201
- args = mock()
202
- args.expects(:my_tran).once # this is validated at the end
203
- c = Class.new
204
- c.class_eval do
205
- include Workflow
206
- def my_transition(args)
207
- args.my_tran
208
- end
209
- workflow do
210
- state :one do
211
- event :my_transition, :transitions_to => :two
212
- end
213
- state :two
214
- end
215
- end
216
- c.new.my_transition!(args)
217
- end
218
-
219
- test 'Single table inheritance (STI)' do
220
- class BigOrder < Order
221
- end
222
-
223
- bo = BigOrder.new
224
- assert bo.submitted?
225
- assert !bo.accepted?
226
- end
227
-
228
- test 'Two-level inheritance' do
229
- class BigOrder < Order
230
- end
231
-
232
- class EvenBiggerOrder < BigOrder
233
- end
234
-
235
- assert EvenBiggerOrder.new.submitted?
236
- end
237
-
238
- test 'Iheritance with workflow definition override' do
239
- class BigOrder < Order
240
- end
241
-
242
- class SpecialBigOrder < BigOrder
243
- workflow do
244
- state :start_big
245
- end
246
- end
247
-
248
- special = SpecialBigOrder.new
249
- assert_equal 'start_big', special.current_state.to_s
250
- end
251
- end
252
-