Skip to content

Commit 50ddd9c

Browse files
committed
Merge pull request #4 from jobready/feature/AV-2123
[AV-2123] Add ability to finish workflow
2 parents 8a2cfab + 4a9fbed commit 50ddd9c

File tree

10 files changed

+240
-25
lines changed

10 files changed

+240
-25
lines changed

.travis.yml

Lines changed: 0 additions & 13 deletions
This file was deleted.

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Decision Tree
22

3+
[![Build status](https://badge.buildkite.com/ed9359ddd05b5fcf16c05eebd63e23d303b3028cd4daf2d32d.svg)](https://buildkite.com/jobready/decision-tree)
4+
35
Decision Tree is an easy way of defining rules/workflows that progress an
46
object's state through a series of boolean decisions.
57

@@ -90,6 +92,73 @@ class Change < ActiveRecord::Base
9092
end
9193
```
9294

95+
##Finishing the Workflow
96+
When a workflow has been completed, and you don't want it to evaluate again, you can call `finish!` on the workflow.
97+
98+
```ruby
99+
class TestWorkflow < DecisionTree::Workflow
100+
def do_something
101+
...
102+
end
103+
104+
start do
105+
do_something
106+
end
107+
108+
decision :do_something do
109+
yes { finish! }
110+
no { finish! }
111+
end
112+
end
113+
```
114+
115+
This will mark the workflow as finished in the state cache, and call `store_steps!` on the carrier, passing the list of `DecisionTree::Step` objects that were taken on the path to finishing the workflow. After it is finished, subsequent invocations of the workflow will no longer execute the workflow, but call `fetch_steps` on the state carrier.
116+
117+
## Storing Steps
118+
Implementation of the storage of the steps is left to the application, and does not need to be implemented. DecisionTree will still work correctly without the step storage, but you will be unable to retrieve/display the steps when reloading the workflow after completion. If this is satisfactory, you can leave `store_steps!` and `fetch_steps` empty.
119+
120+
If you do need to store the final steps, below is an example implementation:
121+
122+
```ruby
123+
class Change < ActiveRecord::Base
124+
has_many :workflow_steps, as: :workflowable
125+
126+
def store_steps!(steps)
127+
workflow_steps.destroy_all
128+
129+
steps.each_with_index do |step, position|
130+
new_step = WorkflowStep.from_decision_step(step, self, position)
131+
new_step.save!
132+
end
133+
end
134+
135+
def fetch_steps
136+
workflow_steps
137+
end
138+
end
139+
140+
class WorkflowStep < ActiveRecord::Base
141+
belongs_to :workflowable, polymorphic: true
142+
default_scope { order(position: :asc) }
143+
144+
delegate :display, to: :decision_step, allow_nil: true
145+
def decision_step
146+
@step ||= DecisionTree::Step.new(step_type, step_info)
147+
end
148+
149+
def self.from_decision_step(decision_step, parent, position)
150+
step_attrs = {
151+
step_type: decision_step.step_type,
152+
step_info: decision_step.step_info,
153+
workflowable: parent,
154+
position: position
155+
}
156+
self.new(step_attrs)
157+
end
158+
end
159+
```
160+
161+
93162
##Displaying the Workflow
94163
Human readable display of workflow steps can be achieved by using `DecisionTree::Step#display`.
95164
E.g.
@@ -123,7 +192,7 @@ Any idempotent calls (identified by a trailing `!`) are defined under `idempoten
123192
Any regular steps (with a yes/no outcome) are defined directly under `workflow_steps`, and contain values for both 'yes', and 'no'. Note that these keys should be defined as strings (explicitly wrapped in quotes), otherwise YAML helpfully converts these to booleans, which will not be matched when looking for a description.
124193

125194
### Implicit Display Values
126-
Semi-friendly display values are still returned if no translation has been defined.
195+
Semi-friendly display values are still returned if no translation has been defined.
127196
For a regular step, the question will be rendered, followed by the answer.
128197

129198
```

lib/decision_tree/options_grabber.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ def yes(&block)
1616
def no(&block)
1717
@no = block
1818
end
19-
end
19+
end

lib/decision_tree/proxy.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ class DecisionTree::Proxy < BasicObject
33
def initialize(proxied_object)
44
@proxied_object = proxied_object
55
end
6-
6+
77
def exit(&block)
88
@proxied_object.instance_eval(&block)
99
throw :exit
1010
false # This isn't chainable.
1111
end
12-
12+
1313
def method_missing(name, *args, &block)
1414
if name.to_s =~ /!\Z/
1515
# Method names ending with a bang are assumed to be non-idempotent,
@@ -23,6 +23,5 @@ def method_missing(name, *args, &block)
2323
else
2424
return @proxied_object.send(name, *args, &block)
2525
end
26-
2726
end
2827
end

lib/decision_tree/store.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,13 @@ def start_workflow(&block)
99
def state!(value)
1010
@state = value
1111
end
12+
13+
# Step storing/fetching
14+
# Do nothing by default. Implement in application.
15+
def store_steps!(steps)
16+
end
17+
18+
# Do nothing by default. Implement in application.
19+
def fetch_steps
20+
end
1221
end

lib/decision_tree/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module DecisionTree
2-
VERSION = "0.0.2"
2+
VERSION = "0.1.0"
33
end

lib/decision_tree/workflow.rb

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ def initialize(store=nil)
2020
initialize_persistent_state
2121
@proxy = DecisionTree::Proxy.new(self)
2222

23+
if finished?
24+
@steps = store.fetch_steps
25+
else
26+
execute_workflow
27+
end
28+
end
29+
30+
def logger
31+
@logger ||= Logger.new(STDERR)
32+
end
33+
34+
def finish!
35+
@nonidempotent_calls << 'finish!'
36+
@steps << DecisionTree::Step.new('Workflow Finished', '__finish_workflow')
37+
store.store_steps!(@steps)
38+
end
39+
40+
def finished?
41+
@nonidempotent_calls.include?('finish!')
42+
end
43+
44+
private
45+
46+
# Actually executes the workflow steps, by executing all the steps from
47+
# either the start, or all previously reached entry points
48+
def execute_workflow
2349
# We're using pessimistic locking here, so this will block until an
2450
# exclusive lock can be obtained on the change.
2551
store.start_workflow do
@@ -37,12 +63,6 @@ def initialize(store=nil)
3763
end
3864
end
3965

40-
def logger
41-
@logger ||= Logger.new(STDERR)
42-
end
43-
44-
private
45-
4666
# We use a DecisionTree::Store to persist workflow across
4767
# instantiations of the workflow object, and guarantee idempotency. The
4868
# actual data is stored as a slug:

script/buildkite.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo '--- setting ruby version'
5+
rbenv local 2.1.3
6+
7+
echo '--- bundling'
8+
bundle install -j $(nproc) --without production --quiet
9+
10+
echo '--- running specs'
11+
REVISION=https://github.com/$BUILDBOX_PROJECT_SLUG/commit/$BUILDBOX_COMMIT
12+
if bundle exec rake spec; then
13+
echo "[Successful] $BUILDBOX_PROJECT_SLUG - Build - $BUILDBOX_BUILD_URL - Commit - $REVISION" | hipchat_room_message -t $HIPCHAT_TOKEN -r $HIPCHAT_ROOM -f "Buildbox" -c "green"
14+
else
15+
echo "[Failed] Build $BUILDBOX_PROJECT_SLUG - Build - $BUILDBOX_BUILD_URL - Commit - $REVISION" | hipchat_room_message -t $HIPCHAT_TOKEN -r $HIPCHAT_ROOM -f "Buildbox" -c "red"
16+
exit 1;
17+
fi

spec/decision_tree/workflow_spec.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,116 @@ def decision_method
186186
expect(store.state).to match(/__start_workflow/)
187187
end
188188
end
189+
190+
describe '.initialize' do
191+
before do
192+
class TestWorkflow < DecisionTree::Workflow
193+
end
194+
195+
allow_any_instance_of(TestWorkflow).to receive(:finished?) { finished }
196+
end
197+
198+
subject { TestWorkflow.new(store) }
199+
200+
context 'when workflow not previously completed' do
201+
let(:finished) { false }
202+
specify 'executes the workflow' do
203+
expect_any_instance_of(TestWorkflow).to receive(:execute_workflow)
204+
subject
205+
end
206+
end
207+
208+
context 'when workflow previously completed' do
209+
let(:finished) { true }
210+
211+
specify 'does not execute the workflow' do
212+
expect_any_instance_of(TestWorkflow).to_not receive(:execute_workflow)
213+
subject
214+
end
215+
216+
specify 'fetches previously executed steps from the store' do
217+
expect(store).to receive(:fetch_steps)
218+
subject
219+
end
220+
end
221+
end
222+
223+
describe 'finish!' do
224+
subject { workflow.instance_variable_get(:@nonidempotent_calls) }
225+
let(:finish!) { workflow.finish! }
226+
227+
before do
228+
class TestWorkflow < DecisionTree::Workflow
229+
start {}
230+
end
231+
end
232+
233+
let(:workflow) { TestWorkflow.new(store) }
234+
235+
specify 'records the finish call' do
236+
finish!
237+
expect(subject).to include('finish!')
238+
end
239+
end
240+
241+
describe 'calling finish!' do
242+
let(:workflow) { TestWorkflow.new(store) }
243+
244+
context 'from a start block' do
245+
246+
before do
247+
class TestWorkflow < DecisionTree::Workflow
248+
start { finish! }
249+
end
250+
end
251+
252+
specify 'calls finish! on the workflow' do
253+
expect_any_instance_of(TestWorkflow).to receive(:finish!)
254+
workflow
255+
end
256+
end
257+
258+
context 'from a decision block' do
259+
before do
260+
class TestWorkflow < DecisionTree::Workflow
261+
def decision_method
262+
true
263+
end
264+
265+
start { decision_method }
266+
267+
decision :decision_method do
268+
yes { finish! }
269+
no { }
270+
end
271+
end
272+
end
273+
274+
specify 'calls finish! on the workflow' do
275+
expect_any_instance_of(TestWorkflow).to receive(:finish!)
276+
workflow
277+
end
278+
end
279+
end
280+
281+
describe '.finished?' do
282+
subject { workflow.finished? }
283+
284+
before do
285+
class TestWorkflow < DecisionTree::Workflow
286+
start {}
287+
end
288+
end
289+
290+
let(:workflow) { TestWorkflow.new(store) }
291+
292+
context 'when workflow has been finished' do
293+
before { workflow.finish! }
294+
specify { expect(subject).to be_truthy }
295+
end
296+
297+
context 'when workflow has not been finished' do
298+
specify { expect(subject).to be_falsey }
299+
end
300+
end
189301
end

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@
2222
config.filter_run :focus
2323
config.filter_run_excluding perf: true
2424
config.order = 'random'
25+
config.color = true
26+
config.formatter = :documentation
2527
end

0 commit comments

Comments
 (0)