seam 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in seam.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Darren Cauthon
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Seam
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'seam'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install seam
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,72 @@
1
+ module Seam
2
+ class Effort
3
+ attr_accessor :completed_steps
4
+ attr_accessor :created_at
5
+ attr_accessor :complete
6
+ attr_accessor :id
7
+ attr_accessor :next_execute_at
8
+ attr_accessor :next_step
9
+ attr_accessor :flow
10
+ attr_accessor :data
11
+ attr_accessor :history
12
+
13
+ class << self
14
+
15
+ def find effort_id
16
+ Seam::Persistence.find_by_effort_id effort_id
17
+ end
18
+
19
+ def find_all_by_step step
20
+ Seam::Persistence.find_all_pending_executions_by_step step
21
+ end
22
+
23
+ def parse document
24
+ effort = Effort.new
25
+ effort.id = document['id']
26
+ effort.created_at = Time.parse(document['created_at'].to_s)
27
+ effort.next_execute_at = document['next_execute_at']
28
+ effort.next_step = document['next_step']
29
+ effort.flow = HashWithIndifferentAccess.new document['flow']
30
+ effort.data = HashWithIndifferentAccess.new document['data']
31
+ effort.history = document['history'].map { |x| HashWithIndifferentAccess.new x }
32
+ effort.completed_steps = document['completed_steps']
33
+ effort.complete = document['complete']
34
+ effort
35
+ end
36
+
37
+ end
38
+
39
+ def initialize
40
+ @completed_steps = []
41
+ @history = []
42
+ @complete = false
43
+ end
44
+
45
+ def save
46
+ existing_record = Seam::Effort.find self.id
47
+ if existing_record
48
+ Seam::Persistence.save self
49
+ else
50
+ Seam::Persistence.create self
51
+ end
52
+ end
53
+
54
+ def complete?
55
+ complete
56
+ end
57
+
58
+ def to_hash
59
+ {
60
+ id: self.id,
61
+ created_at: self.created_at,
62
+ completed_steps: self.completed_steps,
63
+ next_execute_at: self.next_execute_at,
64
+ next_step: self.next_step,
65
+ flow: self.flow,
66
+ data: self.data,
67
+ history: self.history,
68
+ complete: self.complete,
69
+ }
70
+ end
71
+ end
72
+ end
data/lib/seam/flow.rb ADDED
@@ -0,0 +1,44 @@
1
+ module Seam
2
+ class Flow
3
+
4
+ def initialize
5
+ @steps = []
6
+ end
7
+
8
+ def method_missing(meth, *args, &blk)
9
+ @steps << [meth.to_s, args]
10
+ end
11
+
12
+ def start(data = {})
13
+ effort = Seam::Effort.new
14
+ effort.id = SecureRandom.uuid.to_s
15
+ effort.created_at = Time.parse(Time.now.to_s)
16
+ effort.next_execute_at = Time.parse(Time.now.to_s)
17
+ effort.next_step = self.steps.first.name.to_s
18
+ effort.flow = ActiveSupport::HashWithIndifferentAccess.new self.to_hash
19
+ effort.data = ActiveSupport::HashWithIndifferentAccess.new data
20
+ effort.save
21
+ effort
22
+ end
23
+
24
+ def to_hash
25
+ {
26
+ steps: self.steps.map { |x| x.to_hash }
27
+ }
28
+ end
29
+
30
+ def steps
31
+ @steps.map do |x|
32
+ step = Seam::Step.new
33
+ step.name = x[0]
34
+ step.type = "do"
35
+ step.arguments = x[1]
36
+ if step.name.index('branch_on')
37
+ step.name += "_#{x[1][0]}"
38
+ step.type = "branch"
39
+ end
40
+ step
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ module Seam
2
+ module MongoDb
3
+
4
+ def self.collection
5
+ @collection
6
+ end
7
+
8
+ def self.set_collection collection
9
+ @collection = collection
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Seam
2
+ module Persistence
3
+ def self.find_by_effort_id effort_id
4
+ document = Seam::MongoDb.collection.find( { id: effort_id } ).first
5
+ return nil unless document
6
+ Seam::Effort.parse document
7
+ end
8
+
9
+ def self.find_all_pending_executions_by_step step
10
+ Seam::MongoDb.collection
11
+ .find( { next_step: step, next_execute_at: { '$lte' => Time.now } } )
12
+ .map { |x| Seam::Effort.parse x }
13
+ end
14
+
15
+ def self.save effort
16
+ Seam::MongoDb.collection.find( { id: effort.id } )
17
+ .update("$set" => effort.to_hash)
18
+ end
19
+
20
+ def self.create effort
21
+ Seam::MongoDb.collection.insert(effort.to_hash)
22
+ end
23
+ end
24
+ end
data/lib/seam/step.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Seam
2
+ class Step
3
+ attr_accessor :name
4
+ attr_accessor :type
5
+ attr_accessor :arguments
6
+
7
+ def to_hash
8
+ {
9
+ name: name,
10
+ type: type,
11
+ arguments: HashWithIndifferentAccess.new(arguments || {})
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Seam
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,56 @@
1
+ module Seam
2
+ class Worker
3
+ def for(step)
4
+ @step = step
5
+ end
6
+
7
+ def execute effort
8
+ @current_run = HashWithIndifferentAccess.new( {
9
+ started_at: Time.now,
10
+ step: @step.to_s,
11
+ data_before: effort.data,
12
+ } )
13
+ @current_effort = effort
14
+ process
15
+ @current_run[:data_after] = effort.data
16
+ @current_run[:stopped_at] = Time.now
17
+ @current_effort.history << @current_run
18
+ @current_effort.save
19
+ end
20
+
21
+ def execute_all
22
+ Seam::Effort.find_all_by_step(@step.to_s).each do |effort|
23
+ execute effort
24
+ end
25
+ end
26
+
27
+ def eject
28
+ @current_run[:result] = "eject"
29
+ @current_effort.complete = true
30
+ @current_effort.next_step = nil
31
+ @current_effort.save
32
+ end
33
+
34
+ def move_to_next_step
35
+ @current_run[:result] = "move_to_next_step"
36
+ @current_effort.completed_steps << @current_effort.next_step
37
+
38
+ steps = @current_effort.flow['steps'].map { |x| x['name'] }
39
+
40
+ next_step = steps[@current_effort.completed_steps.count]
41
+ @current_effort.next_step = next_step
42
+ @current_effort.complete = next_step.nil?
43
+ @current_effort.save
44
+ end
45
+
46
+ def try_again_in seconds
47
+ try_again_on = Time.now + seconds
48
+
49
+ @current_run[:result] = "try_again_in"
50
+ @current_run[:try_again_on] = try_again_on
51
+
52
+ @current_effort.next_execute_at = try_again_on
53
+ @current_effort.save
54
+ end
55
+ end
56
+ end
data/lib/seam.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'active_support/all'
2
+ require 'active_support/time'
3
+ require 'securerandom'
4
+ require 'moped'
5
+ require 'json'
6
+ Dir[File.dirname(__FILE__) + '/seam/*.rb'].each {|file| require file }
7
+
8
+ module Seam
9
+ end
data/seam.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'seam/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "seam"
8
+ spec.version = Seam::VERSION
9
+ spec.authors = ["Darren Cauthon"]
10
+ spec.email = ["darren@cauthon.com"]
11
+ spec.description = %q{Simple workflows}
12
+ spec.summary = %q{Simple workflows}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "json"
22
+ spec.add_runtime_dependency "active_support"
23
+ spec.add_runtime_dependency "i18n"
24
+ spec.add_runtime_dependency "moped"
25
+ spec.add_runtime_dependency "json"
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "subtle"
29
+ spec.add_development_dependency "contrast"
30
+ spec.add_development_dependency "mocha"
31
+ spec.add_development_dependency "contrast"
32
+ spec.add_development_dependency "timecop"
33
+ end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Seam::Effort do
4
+ before do
5
+ test_moped_session['efforts'].drop
6
+ end
7
+
8
+ let(:flow) do
9
+ f = Seam::Flow.new
10
+ f.step1
11
+ f.step2
12
+ f
13
+ end
14
+
15
+ describe "updating an effort" do
16
+ it "should not create another document in the collection" do
17
+ first_effort = flow.start
18
+ test_moped_session['efforts'].find.count.must_equal 1
19
+ first_effort.save
20
+ test_moped_session['efforts'].find.count.must_equal 1
21
+
22
+ second_effort = flow.start
23
+ test_moped_session['efforts'].find.count.must_equal 2
24
+ second_effort.save
25
+ test_moped_session['efforts'].find.count.must_equal 2
26
+ end
27
+
28
+ it "should update the information" do
29
+ first_effort = flow.start
30
+ second_effort = flow.start
31
+
32
+ first_effort.next_step = 'i_changed_the_first_one'
33
+ first_effort.save
34
+ first_effort.to_hash.contrast_with! Seam::Effort.find(first_effort.id).to_hash, [:id, :created_at]
35
+ second_effort.to_hash.contrast_with! Seam::Effort.find(second_effort.id).to_hash, [:id, :created_at]
36
+
37
+ second_effort.next_step = 'i_changed_the_second_one'
38
+ second_effort.save
39
+ first_effort.to_hash.contrast_with! Seam::Effort.find(first_effort.id).to_hash, [:id, :created_at]
40
+ second_effort.to_hash.contrast_with! Seam::Effort.find(second_effort.id).to_hash, [:id, :created_at]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "flow" do
4
+ before do
5
+ test_moped_session['efforts'].drop
6
+ end
7
+
8
+ after do
9
+ Timecop.return
10
+ end
11
+
12
+ describe "a useful example" do
13
+ let(:flow) do
14
+ f = Seam::Flow.new
15
+ f.wait_for_attempting_contact_stage limit: 2.weeks
16
+ f.determine_if_postcard_should_be_sent
17
+ f.send_postcard_if_necessary
18
+ f
19
+ end
20
+
21
+ describe "steps that must be taken" do
22
+ before do
23
+ flow
24
+ end
25
+
26
+ it "should not throw an error" do
27
+ flow.steps.count.must_equal 3
28
+ end
29
+
30
+ it "should set the name of the three steps" do
31
+ flow.steps[0].name.must_equal "wait_for_attempting_contact_stage"
32
+ flow.steps[1].name.must_equal "determine_if_postcard_should_be_sent"
33
+ flow.steps[2].name.must_equal "send_postcard_if_necessary"
34
+ end
35
+
36
+ it "should set the step types of the three steps" do
37
+ flow.steps[0].type.must_equal "do"
38
+ flow.steps[1].type.must_equal "do"
39
+ flow.steps[2].type.must_equal "do"
40
+ end
41
+
42
+ it "should set the arguments as well" do
43
+ flow.steps[0].arguments.must_equal [{ limit: 14.days.to_i }]
44
+ flow.steps[1].arguments.must_equal []
45
+ flow.steps[2].arguments.must_equal []
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "a more useful example" do
51
+
52
+ describe "starting an effort" do
53
+ let(:flow) do
54
+ flow = Seam::Flow.new
55
+ flow.do_something
56
+ flow.do_something_else
57
+ flow
58
+ end
59
+
60
+ let(:now) { Time.parse('1/1/2011') }
61
+
62
+ before do
63
+ Timecop.freeze now
64
+
65
+ @expected_uuid = SecureRandom.uuid.to_s
66
+ SecureRandom.expects(:uuid).returns @expected_uuid
67
+
68
+ @effort = flow.start( { first_name: 'John' } )
69
+ end
70
+
71
+ it "should mark no steps as completed" do
72
+ @effort.completed_steps.count.must_equal 0
73
+ end
74
+
75
+ it "should stamp the effort with a uuid" do
76
+ @effort.id.must_equal @expected_uuid
77
+ end
78
+
79
+ it "should stamp the create date" do
80
+ @effort.created_at.must_equal now
81
+ end
82
+
83
+ it "should stamp the next execute date" do
84
+ @effort.next_execute_at.must_equal now
85
+ end
86
+
87
+ it "should stamp the next step name" do
88
+ @effort.next_step.must_equal "do_something"
89
+ end
90
+
91
+ it "should save an effort in the db" do
92
+ effort = Seam::Effort.find @effort.id
93
+ effort.to_hash.contrast_with! @effort.to_hash, [:id, :created_at]
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,583 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "worker" do
4
+
5
+ before do
6
+ test_moped_session['efforts'].drop
7
+ end
8
+
9
+ after do
10
+ Timecop.return
11
+ end
12
+
13
+ describe "move_to_next_step" do
14
+ it "should work" do
15
+ flow = Seam::Flow.new
16
+ flow.apple
17
+ flow.orange
18
+
19
+ effort = flow.start( { first_name: 'John' } )
20
+ effort = Seam::Effort.find(effort.id)
21
+
22
+ effort.next_step.must_equal "apple"
23
+
24
+ apple_worker = Seam::Worker.new
25
+ apple_worker.for(:apple)
26
+ def apple_worker.process
27
+ move_to_next_step
28
+ end
29
+
30
+ apple_worker.execute effort
31
+
32
+ effort = Seam::Effort.find(effort.id)
33
+ effort.next_step.must_equal "orange"
34
+ end
35
+ end
36
+
37
+ describe "try_again_in" do
38
+
39
+ let(:effort) do
40
+ flow = Seam::Flow.new
41
+ flow.apple
42
+ flow.orange
43
+
44
+ e = flow.start( { first_name: 'John' } )
45
+ Seam::Effort.find(e.id)
46
+ end
47
+
48
+ before do
49
+ Timecop.freeze Time.parse('3/4/2013')
50
+ effort.next_step.must_equal "apple"
51
+
52
+ apple_worker = Seam::Worker.new
53
+ apple_worker.for(:apple)
54
+ def apple_worker.process
55
+ try_again_in 1.day
56
+ end
57
+
58
+ apple_worker.execute effort
59
+ end
60
+
61
+ it "should not update the next step" do
62
+ fresh_effort = Seam::Effort.find(effort.id)
63
+ fresh_effort.next_step.must_equal "apple"
64
+ end
65
+
66
+ it "should not update the next execute date" do
67
+ fresh_effort = Seam::Effort.find(effort.id)
68
+ fresh_effort.next_execute_at.must_equal Time.parse('4/4/2013')
69
+ end
70
+ end
71
+
72
+ describe "more copmlex example" do
73
+
74
+ let(:effort1) do
75
+ flow = Seam::Flow.new
76
+ flow.grape
77
+ flow.mango
78
+
79
+ e = flow.start( { status: 'Good' } )
80
+ Seam::Effort.find(e.id)
81
+ end
82
+
83
+ let(:effort2) do
84
+ flow = Seam::Flow.new
85
+ flow.grape
86
+ flow.mango
87
+
88
+ e = flow.start( { status: 'Bad' } )
89
+ Seam::Effort.find(e.id)
90
+ end
91
+
92
+ before do
93
+ Timecop.freeze Time.parse('1/6/2013')
94
+
95
+ apple_worker = Seam::Worker.new
96
+ apple_worker.for(:apple)
97
+ def apple_worker.process
98
+ if @current_effort.data[:status] == 'Good'
99
+ move_to_next_step
100
+ else
101
+ try_again_in 1.day
102
+ end
103
+ end
104
+
105
+ apple_worker.execute effort1
106
+ apple_worker.execute effort2
107
+ end
108
+
109
+ it "should move the first effort forward" do
110
+ fresh_effort = Seam::Effort.find(effort1.id)
111
+ fresh_effort.next_step.must_equal "mango"
112
+ end
113
+
114
+ it "should keep the second effort at the same step" do
115
+ fresh_effort = Seam::Effort.find(effort2.id)
116
+ fresh_effort.next_step.must_equal "grape"
117
+ fresh_effort.next_execute_at.must_equal Time.parse('2/6/2013')
118
+ end
119
+ end
120
+
121
+ describe "processing all pending steps for one effort" do
122
+ let(:effort1_creator) do
123
+ ->() do
124
+ flow = Seam::Flow.new
125
+ flow.banana
126
+ flow.mango
127
+
128
+ e = flow.start
129
+ Seam::Effort.find(e.id)
130
+ end
131
+ end
132
+
133
+ let(:effort2_creator) do
134
+ ->() do
135
+ flow = Seam::Flow.new
136
+ flow.apple
137
+ flow.orange
138
+
139
+ e = flow.start
140
+ Seam::Effort.find(e.id)
141
+ end
142
+ end
143
+
144
+ let(:apple_worker) do
145
+ apple_worker = Seam::Worker.new
146
+ apple_worker.for(:apple)
147
+
148
+ apple_worker.class_eval do
149
+ attr_accessor :count
150
+ end
151
+
152
+ def apple_worker.process
153
+ self.count += 1
154
+ end
155
+
156
+ apple_worker.count = 0
157
+ apple_worker
158
+ end
159
+
160
+ before do
161
+ Timecop.freeze Time.parse('1/6/2013')
162
+
163
+ effort1_creator.call
164
+ effort1_creator.call
165
+ effort1_creator.call
166
+ effort2_creator.call
167
+ effort2_creator.call
168
+
169
+ apple_worker.execute_all
170
+ end
171
+
172
+ it "should call the apple worker for the record in question" do
173
+ apple_worker.count.must_equal 2
174
+ end
175
+ end
176
+
177
+ describe "a more realistic example" do
178
+
179
+ let(:flow) do
180
+ flow = Seam::Flow.new
181
+ flow.wait_for_attempting_contact_stage
182
+ flow.determine_if_postcard_should_be_sent
183
+ flow.send_postcard_if_necessary
184
+ flow
185
+ end
186
+
187
+ let(:effort_creator) do
188
+ ->() do
189
+ e = flow.start
190
+ Seam::Effort.find(e.id)
191
+ end
192
+ end
193
+
194
+ let(:wait_for_attempting_contact_stage_worker) do
195
+ worker = Seam::Worker.new
196
+ worker.for(:wait_for_attempting_contact_stage)
197
+
198
+ def worker.process
199
+ @current_effort.data['hit 1'] ||= 0
200
+ @current_effort.data['hit 1'] += 1
201
+ move_to_next_step
202
+ end
203
+
204
+ worker
205
+ end
206
+
207
+ let(:determine_if_postcard_should_be_sent_worker) do
208
+ worker = Seam::Worker.new
209
+ worker.for(:determine_if_postcard_should_be_sent)
210
+
211
+ def worker.process
212
+ @current_effort.data['hit 2'] ||= 0
213
+ @current_effort.data['hit 2'] += 1
214
+ move_to_next_step
215
+ end
216
+
217
+ worker
218
+ end
219
+
220
+ let(:send_postcard_if_necessary_worker) do
221
+ worker = Seam::Worker.new
222
+ worker.for(:send_postcard_if_necessary)
223
+
224
+ def worker.process
225
+ @current_effort.data['hit 3'] ||= 0
226
+ @current_effort.data['hit 3'] += 1
227
+ move_to_next_step
228
+ end
229
+
230
+ worker
231
+ end
232
+
233
+ before do
234
+ Timecop.freeze Time.parse('1/6/2013')
235
+ end
236
+
237
+ it "should progress through the story" do
238
+
239
+ # SETUP
240
+ effort = effort_creator.call
241
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
242
+
243
+ # FIRST WAVE
244
+ send_postcard_if_necessary_worker.execute_all
245
+ determine_if_postcard_should_be_sent_worker.execute_all
246
+ wait_for_attempting_contact_stage_worker.execute_all
247
+
248
+ effort = Seam::Effort.find effort.id
249
+ effort.next_step.must_equal "determine_if_postcard_should_be_sent"
250
+
251
+ effort.complete?.must_equal false
252
+
253
+ # SECOND WAVE
254
+ send_postcard_if_necessary_worker.execute_all
255
+ determine_if_postcard_should_be_sent_worker.execute_all
256
+ wait_for_attempting_contact_stage_worker.execute_all
257
+
258
+ effort = Seam::Effort.find effort.id
259
+ effort.next_step.must_equal "send_postcard_if_necessary"
260
+
261
+ # THIRD WAVE
262
+ send_postcard_if_necessary_worker.execute_all
263
+ determine_if_postcard_should_be_sent_worker.execute_all
264
+ wait_for_attempting_contact_stage_worker.execute_all
265
+
266
+ effort = Seam::Effort.find effort.id
267
+ effort.next_step.must_equal nil
268
+
269
+ effort.data['hit 1'].must_equal 1
270
+ effort.data['hit 2'].must_equal 1
271
+ effort.data['hit 3'].must_equal 1
272
+
273
+ effort.complete?.must_equal true
274
+
275
+ # FUTURE WAVES
276
+ send_postcard_if_necessary_worker.execute_all
277
+ determine_if_postcard_should_be_sent_worker.execute_all
278
+ wait_for_attempting_contact_stage_worker.execute_all
279
+ send_postcard_if_necessary_worker.execute_all
280
+ determine_if_postcard_should_be_sent_worker.execute_all
281
+ wait_for_attempting_contact_stage_worker.execute_all
282
+ send_postcard_if_necessary_worker.execute_all
283
+ determine_if_postcard_should_be_sent_worker.execute_all
284
+ wait_for_attempting_contact_stage_worker.execute_all
285
+
286
+ effort = Seam::Effort.find effort.id
287
+ effort.next_step.must_equal nil
288
+
289
+ effort.data['hit 1'].must_equal 1
290
+ effort.data['hit 2'].must_equal 1
291
+ effort.data['hit 3'].must_equal 1
292
+
293
+ end
294
+ end
295
+
296
+ describe "a more realistic example with waiting" do
297
+
298
+ let(:flow) do
299
+ flow = Seam::Flow.new
300
+ flow.wait_for_attempting_contact_stage
301
+ flow.determine_if_postcard_should_be_sent
302
+ flow.send_postcard_if_necessary
303
+ flow
304
+ end
305
+
306
+ let(:effort_creator) do
307
+ ->() do
308
+ e = flow.start
309
+ Seam::Effort.find(e.id)
310
+ end
311
+ end
312
+
313
+ let(:wait_for_attempting_contact_stage_worker) do
314
+ worker = Seam::Worker.new
315
+ worker.for(:wait_for_attempting_contact_stage)
316
+
317
+ def worker.process
318
+ @current_effort.data['hit 1'] ||= 0
319
+ @current_effort.data['hit 1'] += 1
320
+ if Time.now >= Time.parse('28/12/2013')
321
+ move_to_next_step
322
+ else
323
+ try_again_in 1.day
324
+ end
325
+ end
326
+
327
+ worker
328
+ end
329
+
330
+ let(:determine_if_postcard_should_be_sent_worker) do
331
+ worker = Seam::Worker.new
332
+ worker.for(:determine_if_postcard_should_be_sent)
333
+
334
+ def worker.process
335
+ @current_effort.data['hit 2'] ||= 0
336
+ @current_effort.data['hit 2'] += 1
337
+ move_to_next_step
338
+ end
339
+
340
+ worker
341
+ end
342
+
343
+ let(:send_postcard_if_necessary_worker) do
344
+ worker = Seam::Worker.new
345
+ worker.for(:send_postcard_if_necessary)
346
+
347
+ def worker.process
348
+ @current_effort.data['hit 3'] ||= 0
349
+ @current_effort.data['hit 3'] += 1
350
+ move_to_next_step
351
+ end
352
+
353
+ worker
354
+ end
355
+
356
+ before do
357
+ Timecop.freeze Time.parse('25/12/2013')
358
+ end
359
+
360
+ it "should progress through the story" do
361
+
362
+ # SETUP
363
+ effort = effort_creator.call
364
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
365
+
366
+ # FIRST DAY
367
+ send_postcard_if_necessary_worker.execute_all
368
+ determine_if_postcard_should_be_sent_worker.execute_all
369
+ wait_for_attempting_contact_stage_worker.execute_all
370
+
371
+ effort = Seam::Effort.find effort.id
372
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
373
+
374
+ send_postcard_if_necessary_worker.execute_all
375
+ determine_if_postcard_should_be_sent_worker.execute_all
376
+ wait_for_attempting_contact_stage_worker.execute_all
377
+ send_postcard_if_necessary_worker.execute_all
378
+ determine_if_postcard_should_be_sent_worker.execute_all
379
+ wait_for_attempting_contact_stage_worker.execute_all
380
+
381
+ effort = Seam::Effort.find effort.id
382
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
383
+
384
+ Timecop.freeze Time.parse('29/12/2013')
385
+
386
+ send_postcard_if_necessary_worker.execute_all
387
+ determine_if_postcard_should_be_sent_worker.execute_all
388
+ wait_for_attempting_contact_stage_worker.execute_all
389
+
390
+ effort = Seam::Effort.find effort.id
391
+ effort.next_step.must_equal "determine_if_postcard_should_be_sent"
392
+ effort.data['hit 1'].must_equal 2
393
+ end
394
+ end
395
+
396
+ describe "tracking history" do
397
+
398
+ let(:flow) do
399
+ flow = Seam::Flow.new
400
+ flow.wait_for_attempting_contact_stage
401
+ flow.determine_if_postcard_should_be_sent
402
+ flow.send_postcard_if_necessary
403
+ flow
404
+ end
405
+
406
+ let(:effort_creator) do
407
+ ->(values = {}) do
408
+ e = flow.start values
409
+ Seam::Effort.find(e.id)
410
+ end
411
+ end
412
+
413
+ let(:wait_for_attempting_contact_stage_worker) do
414
+ worker = Seam::Worker.new
415
+ worker.for(:wait_for_attempting_contact_stage)
416
+
417
+ def worker.process
418
+ @current_effort.data['hit 1'] ||= 0
419
+ @current_effort.data['hit 1'] += 1
420
+ if Time.now >= Time.parse('28/12/2013')
421
+ move_to_next_step
422
+ else
423
+ try_again_in 1.day
424
+ end
425
+ end
426
+
427
+ worker
428
+ end
429
+
430
+ let(:determine_if_postcard_should_be_sent_worker) do
431
+ worker = Seam::Worker.new
432
+ worker.for(:determine_if_postcard_should_be_sent)
433
+
434
+ def worker.process
435
+ @current_effort.data['hit 2'] ||= 0
436
+ @current_effort.data['hit 2'] += 1
437
+ move_to_next_step
438
+ end
439
+
440
+ worker
441
+ end
442
+
443
+ let(:send_postcard_if_necessary_worker) do
444
+ worker = Seam::Worker.new
445
+ worker.for(:send_postcard_if_necessary)
446
+
447
+ def worker.process
448
+ @current_effort.data['hit 3'] ||= 0
449
+ @current_effort.data['hit 3'] += 1
450
+ move_to_next_step
451
+ end
452
+
453
+ worker
454
+ end
455
+
456
+ before do
457
+ Timecop.freeze Time.parse('26/12/2013')
458
+ end
459
+
460
+ it "should progress through the story" do
461
+
462
+ # SETUP
463
+ effort = effort_creator.call({ first_name: 'DARREN' })
464
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
465
+
466
+ # FIRST DAY
467
+ send_postcard_if_necessary_worker.execute_all
468
+ determine_if_postcard_should_be_sent_worker.execute_all
469
+ wait_for_attempting_contact_stage_worker.execute_all
470
+
471
+ effort = Seam::Effort.find effort.id
472
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
473
+
474
+ effort.history.count.must_equal 1
475
+ effort.history[0].contrast_with!( {
476
+ "started_at"=> Time.now,
477
+ "step"=>"wait_for_attempting_contact_stage",
478
+ "stopped_at" => Time.now,
479
+ "data_before" => { "first_name" => "DARREN" } ,
480
+ "data_after" => { "first_name" => "DARREN", "hit 1" => 1 }
481
+ } )
482
+
483
+ send_postcard_if_necessary_worker.execute_all
484
+ determine_if_postcard_should_be_sent_worker.execute_all
485
+ wait_for_attempting_contact_stage_worker.execute_all
486
+ send_postcard_if_necessary_worker.execute_all
487
+ determine_if_postcard_should_be_sent_worker.execute_all
488
+ wait_for_attempting_contact_stage_worker.execute_all
489
+
490
+ effort = Seam::Effort.find effort.id
491
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
492
+
493
+ effort.history.count.must_equal 1
494
+ effort.history[0].contrast_with!({"started_at"=> Time.now, "step"=>"wait_for_attempting_contact_stage", "stopped_at" => Time.now, "result" => "try_again_in", "try_again_on" => Time.now + 1.day } )
495
+
496
+ # THE NEXT DAY
497
+ Timecop.freeze Time.parse('27/12/2013')
498
+
499
+ send_postcard_if_necessary_worker.execute_all
500
+ determine_if_postcard_should_be_sent_worker.execute_all
501
+ wait_for_attempting_contact_stage_worker.execute_all
502
+
503
+ effort = Seam::Effort.find effort.id
504
+ effort.next_step.must_equal "wait_for_attempting_contact_stage"
505
+
506
+ effort.history.count.must_equal 2
507
+ effort.history[1].contrast_with!({"started_at"=> Time.now, "step"=>"wait_for_attempting_contact_stage", "stopped_at" => Time.now, "result" => "try_again_in" } )
508
+
509
+ # THE NEXT DAY
510
+ Timecop.freeze Time.parse('28/12/2013')
511
+
512
+ send_postcard_if_necessary_worker.execute_all
513
+ determine_if_postcard_should_be_sent_worker.execute_all
514
+ wait_for_attempting_contact_stage_worker.execute_all
515
+
516
+ effort = Seam::Effort.find effort.id
517
+ effort.next_step.must_equal "determine_if_postcard_should_be_sent"
518
+
519
+ effort.history.count.must_equal 3
520
+ effort.history[2].contrast_with!({"started_at"=> Time.now, "step"=>"wait_for_attempting_contact_stage", "stopped_at" => Time.now, "result" => "move_to_next_step" } )
521
+
522
+ # KEEP GOING
523
+ send_postcard_if_necessary_worker.execute_all
524
+ determine_if_postcard_should_be_sent_worker.execute_all
525
+ wait_for_attempting_contact_stage_worker.execute_all
526
+ effort = Seam::Effort.find effort.id
527
+ effort.next_step.must_equal "send_postcard_if_necessary"
528
+
529
+ effort.history.count.must_equal 4
530
+ effort.history[3].contrast_with!({"started_at"=> Time.now, "step"=>"determine_if_postcard_should_be_sent", "stopped_at" => Time.now, "result" => "move_to_next_step" } )
531
+
532
+ # KEEP GOING
533
+ send_postcard_if_necessary_worker.execute_all
534
+ determine_if_postcard_should_be_sent_worker.execute_all
535
+ wait_for_attempting_contact_stage_worker.execute_all
536
+ effort = Seam::Effort.find effort.id
537
+ effort.next_step.must_equal nil
538
+
539
+ effort.history.count.must_equal 5
540
+ effort.history[4].contrast_with!({"started_at"=> Time.now, "step"=>"send_postcard_if_necessary", "stopped_at" => Time.now, "result" => "move_to_next_step" } )
541
+ end
542
+ end
543
+
544
+ describe "eject" do
545
+
546
+ let(:effort) do
547
+ flow = Seam::Flow.new
548
+ flow.apple
549
+ flow.orange
550
+
551
+ e = flow.start( { first_name: 'John' } )
552
+ Seam::Effort.find(e.id)
553
+ end
554
+
555
+ before do
556
+ Timecop.freeze Time.parse('5/11/2013')
557
+ effort.next_step.must_equal "apple"
558
+
559
+ apple_worker = Seam::Worker.new
560
+ apple_worker.for(:apple)
561
+ def apple_worker.process
562
+ eject
563
+ end
564
+
565
+ apple_worker.execute effort
566
+ end
567
+
568
+ it "should mark the step as completed" do
569
+ fresh_effort = Seam::Effort.find(effort.id)
570
+ fresh_effort.complete?.must_equal true
571
+ end
572
+
573
+ it "should mark the next step to nil" do
574
+ fresh_effort = Seam::Effort.find(effort.id)
575
+ fresh_effort.next_step.nil?.must_equal true
576
+ end
577
+
578
+ it "should mark the history" do
579
+ effort.history[0].contrast_with!({"step"=>"apple", "result" => "eject" } )
580
+ end
581
+
582
+ end
583
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/seam')
2
+ require 'minitest/autorun'
3
+ require 'minitest/spec'
4
+ require 'minitest/pride'
5
+ require 'subtle'
6
+ require 'timecop'
7
+ require 'contrast'
8
+ require 'mocha/setup'
9
+
10
+ def test_moped_session
11
+ session = Moped::Session.new([ "127.0.0.1:27017" ])
12
+ session.use "seam_test"
13
+ end
14
+
15
+ Seam::MongoDb.set_collection test_moped_session['efforts']
metadata ADDED
@@ -0,0 +1,266 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Darren Cauthon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: active_support
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: i18n
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: moped
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: json
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: bundler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '1.3'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.3'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: subtle
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: contrast
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: mocha
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: contrast
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ - !ruby/object:Gem::Dependency
191
+ name: timecop
192
+ requirement: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ description: Simple workflows
207
+ email:
208
+ - darren@cauthon.com
209
+ executables: []
210
+ extensions: []
211
+ extra_rdoc_files: []
212
+ files:
213
+ - .gitignore
214
+ - Gemfile
215
+ - LICENSE.txt
216
+ - README.md
217
+ - Rakefile
218
+ - lib/seam.rb
219
+ - lib/seam/effort.rb
220
+ - lib/seam/flow.rb
221
+ - lib/seam/mongo_db.rb
222
+ - lib/seam/persistence.rb
223
+ - lib/seam/step.rb
224
+ - lib/seam/version.rb
225
+ - lib/seam/worker.rb
226
+ - seam.gemspec
227
+ - spec/seam/effort_spec.rb
228
+ - spec/seam/flow_spec.rb
229
+ - spec/seam/worker_spec.rb
230
+ - spec/spec_helper.rb
231
+ homepage: ''
232
+ licenses:
233
+ - MIT
234
+ post_install_message:
235
+ rdoc_options: []
236
+ require_paths:
237
+ - lib
238
+ required_ruby_version: !ruby/object:Gem::Requirement
239
+ none: false
240
+ requirements:
241
+ - - ! '>='
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ segments:
245
+ - 0
246
+ hash: 1454651581186774459
247
+ required_rubygems_version: !ruby/object:Gem::Requirement
248
+ none: false
249
+ requirements:
250
+ - - ! '>='
251
+ - !ruby/object:Gem::Version
252
+ version: '0'
253
+ segments:
254
+ - 0
255
+ hash: 1454651581186774459
256
+ requirements: []
257
+ rubyforge_project:
258
+ rubygems_version: 1.8.24
259
+ signing_key:
260
+ specification_version: 3
261
+ summary: Simple workflows
262
+ test_files:
263
+ - spec/seam/effort_spec.rb
264
+ - spec/seam/flow_spec.rb
265
+ - spec/seam/worker_spec.rb
266
+ - spec/spec_helper.rb