empipelines 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source :gemcutter
2
+
3
+ gem 'rake'
4
+ gem 'json'
5
+ gem 'eventmachine'
6
+ gem 'em-http-request'
7
+ gem 'amqp'
8
+
9
+ group :cucumber do
10
+ gem 'activesupport'
11
+ gem 'sinatra'
12
+ gem 'cucumber'
13
+ gem 'rspec'
14
+ gem 'bunny'
15
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,69 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.1.3)
5
+ multi_json (~> 1.0)
6
+ addressable (2.2.6)
7
+ amq-client (0.8.7)
8
+ amq-protocol (>= 0.8.4)
9
+ eventmachine
10
+ amq-protocol (0.8.4)
11
+ amqp (0.8.4)
12
+ amq-client (~> 0.8.7)
13
+ amq-protocol (~> 0.8.4)
14
+ eventmachine
15
+ builder (3.0.0)
16
+ bunny (0.7.8)
17
+ cucumber (1.1.4)
18
+ builder (>= 2.1.2)
19
+ diff-lcs (>= 1.1.2)
20
+ gherkin (~> 2.7.1)
21
+ json (>= 1.4.6)
22
+ term-ansicolor (>= 1.0.6)
23
+ diff-lcs (1.1.3)
24
+ em-http-request (1.0.0)
25
+ addressable (>= 2.2.3)
26
+ em-socksify
27
+ eventmachine (>= 1.0.0.beta.3)
28
+ http_parser.rb (>= 0.5.2)
29
+ em-socksify (0.1.0)
30
+ eventmachine
31
+ eventmachine (1.0.0.beta.4)
32
+ gherkin (2.7.1)
33
+ json (>= 1.4.6)
34
+ http_parser.rb (0.5.3)
35
+ json (1.6.3)
36
+ multi_json (1.0.4)
37
+ rack (1.3.5)
38
+ rack-protection (1.1.4)
39
+ rack
40
+ rake (0.9.2.2)
41
+ rspec (2.7.0)
42
+ rspec-core (~> 2.7.0)
43
+ rspec-expectations (~> 2.7.0)
44
+ rspec-mocks (~> 2.7.0)
45
+ rspec-core (2.7.1)
46
+ rspec-expectations (2.7.0)
47
+ diff-lcs (~> 1.1.2)
48
+ rspec-mocks (2.7.0)
49
+ sinatra (1.3.1)
50
+ rack (~> 1.3, >= 1.3.4)
51
+ rack-protection (~> 1.1, >= 1.1.2)
52
+ tilt (~> 1.3, >= 1.3.3)
53
+ term-ansicolor (1.0.7)
54
+ tilt (1.3.3)
55
+
56
+ PLATFORMS
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ activesupport
61
+ amqp
62
+ bunny
63
+ cucumber
64
+ em-http-request
65
+ eventmachine
66
+ json
67
+ rake
68
+ rspec
69
+ sinatra
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # EM Pipelines
data/Rakefile ADDED
@@ -0,0 +1,168 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'cucumber/rake/task'
4
+ require 'rubygems'
5
+ require 'rake'
6
+ require 'date'
7
+
8
+ #############################################################################
9
+ #
10
+ # Helper functions
11
+ #
12
+ #############################################################################
13
+
14
+ def name
15
+ @name ||= Dir['*.gemspec'].first.split('.').first
16
+ end
17
+
18
+ def version
19
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
20
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
21
+ end
22
+
23
+ def date
24
+ Date.today.to_s
25
+ end
26
+
27
+ def rubyforge_project
28
+ name
29
+ end
30
+
31
+ def gemspec_file
32
+ "#{name}.gemspec"
33
+ end
34
+
35
+ def gemspec
36
+ @gemspec ||= eval(IO.read(gemspec_file))
37
+ end
38
+
39
+ def gem_file
40
+ gemspec.file_name
41
+ end
42
+
43
+ def replace_header(head, header_name)
44
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
45
+ end
46
+
47
+ #############################################################################
48
+ #
49
+ # Standard tasks
50
+ #
51
+ #############################################################################
52
+ task :default => :pre_checkin
53
+
54
+ Cucumber::Rake::Task.new(:non_wip_features) do |t|
55
+ t.cucumber_opts = "--format pretty --tag ~@wip"
56
+ end
57
+
58
+ Cucumber::Rake::Task.new(:all_features) do |t|
59
+ t.cucumber_opts = "--format pretty"
60
+ end
61
+
62
+ desc "Runs specs"
63
+ task :specs do
64
+ all = FileList['spec/**/*_spec.rb']
65
+ sh "rspec --color #{all}"
66
+ end
67
+
68
+ desc "Resets localhost's rabbitmq"
69
+ task :reset_rabbitmq do
70
+ sh 'rabbitmqctl stop_app'
71
+ sh 'rabbitmqctl reset'
72
+ sh 'rabbitmqctl start_app'
73
+ end
74
+
75
+ desc "Run cucumber tests for finished features"
76
+ task :features do
77
+
78
+ end
79
+
80
+ desc "Run cucumber tests for all features, including work in progress"
81
+ task :all_features do
82
+
83
+ end
84
+
85
+ task :ci => [:specs, :features, :build]
86
+
87
+ desc "MUST BE RUN (AND PASS!) BEFORE CHECKING IN CODE!"
88
+ task :pre_checkin => [:reset_rabbitmq, :ci]
89
+
90
+ #############################################################################
91
+ #
92
+ # Custom tasks (add your own tasks here)
93
+ #
94
+ #############################################################################
95
+
96
+
97
+
98
+ #############################################################################
99
+ #
100
+ # Packaging tasks
101
+ #
102
+ #############################################################################
103
+
104
+ task :release => :ci do
105
+ Dir.chdir File.dirname(__FILE__)
106
+ unless `git branch` =~ /^\* master$/
107
+ puts "You must be on the master branch to release!"
108
+ exit!
109
+ end
110
+ if `git fetch --tags && git tag`.split(/\n/).include?(gem_file)
111
+ raise "Version #{gem_file} already deployed"
112
+ end
113
+ sh <<-END
114
+ git commit -a --allow-empty -m 'Release #{gem_file}'
115
+ git tag -a #{gem_file} -m 'Version #{gem_file}'
116
+ git push origin master
117
+ git push origin --tags
118
+ END
119
+ end
120
+
121
+ desc "Build #{gem_file} into the pkg directory"
122
+ task :build => :gemspec do
123
+ sh "mkdir -p pkg"
124
+ sh "gem build #{gemspec_file}"
125
+ sh "mv #{gem_file} pkg"
126
+ end
127
+
128
+ desc "Generate #{gemspec_file}"
129
+ task :gemspec => :validate do
130
+ # read spec file and split out manifest section
131
+ spec = File.read(gemspec_file)
132
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
133
+
134
+ # replace name version and date
135
+ replace_header(head, :name)
136
+ replace_header(head, :version)
137
+ replace_header(head, :date)
138
+ #comment this out if your rubyforge_project has a different name
139
+ replace_header(head, :rubyforge_project)
140
+
141
+ # determine file list from git ls-files
142
+ files = `git ls-files`.
143
+ split("\n").
144
+ sort.
145
+ reject { |file| file =~ /^\./ }.
146
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
147
+ map { |file| " #{file}" }.
148
+ join("\n")
149
+
150
+ # piece file back together and write
151
+ manifest = " s.files = %w[\n#{files}\n ]\n"
152
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
153
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
154
+ puts "Updated #{gemspec_file}"
155
+ end
156
+
157
+ desc "Validate #{gemspec_file}"
158
+ task :validate do
159
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
160
+ unless libfiles.empty?
161
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
162
+ exit!
163
+ end
164
+ unless Dir['VERSION*'].empty?
165
+ puts "A `VERSION` file at root level violates Gem best practices."
166
+ exit!
167
+ end
168
+ end
@@ -0,0 +1,61 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+ s.rubygems_version = '1.3.5'
5
+ ## Leave these as is they will be modified for you by the rake gemspec task.
6
+ s.name = 'empipelines'
7
+ s.version = '0.1'
8
+ s.date = '2011-12-19'
9
+ s.rubyforge_project = 'empipelines'
10
+
11
+ s.summary = "Simple Event Handling Pipeline Architecture for EventMachine"
12
+ s.description = "Simple Event Handling Pipeline Architecture for EventMachine"
13
+
14
+ s.authors = ["Tobias Schmidt", "Phil Calcado"]
15
+ s.email = 'pcalcado+empipelines@gmail.com'
16
+ s.homepage = 'http://github.com/pcalcado/empipelines'
17
+
18
+ s.require_paths = %w[lib]
19
+ #s.executables = ["empipelines"]
20
+ #s.default_executable = 'empipelines'
21
+
22
+ ## Specify any RDoc options here.
23
+ s.rdoc_options = ["--charset=UTF-8"]
24
+ s.extra_rdoc_files = %w[README.md]
25
+
26
+ ## List your runtime dependencies here.
27
+ #s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"])
28
+ #s.add_development_dependency('bacon', [">= 1.1.0])
29
+ #s.add_development_dependency('rspec', [">= 1.3.0"])
30
+
31
+ ## DO NOT REMOVE THE MANIFEST COMMENTS, they are used as delimiters by the task.
32
+ # = MANIFEST =
33
+ s.files = %w[
34
+ Gemfile
35
+ Gemfile.lock
36
+ README.md
37
+ Rakefile
38
+ empipelines.gemspec
39
+ lib/empipelines.rb
40
+ lib/empipelines/amqp_event_source.rb
41
+ lib/empipelines/batch_event_source.rb
42
+ lib/empipelines/event_pipeline.rb
43
+ lib/empipelines/list_event_source.rb
44
+ lib/empipelines/message.rb
45
+ lib/empipelines/periodic_event_source.rb
46
+ lib/empipelines/pipeline.rb
47
+ spec/empipelines/amqp_event_source_spec.rb
48
+ spec/empipelines/batch_event_source_spec.rb
49
+ spec/empipelines/event_pipeline_spec.rb
50
+ spec/empipelines/list_event_source_spec.rb
51
+ spec/empipelines/message_spec.rb
52
+ spec/empipelines/periodic_event_source_spec.rb
53
+ spec/empipelines/pipeline_spec.rb
54
+ spec/spec_helper.rb
55
+ spec/stage_helper.rb
56
+ ]
57
+ # = MANIFEST =
58
+
59
+ ## Test files will be grabbed from the file list.
60
+ s.test_files = s.files.select { |path| path =~ /^spec\/*_spec\.rb/ }
61
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+
3
+ module Pipelines
4
+ class AmqpEventSource
5
+ def initialize(queue, event_name)
6
+ @queue, @event_name = queue, event_name
7
+ end
8
+
9
+ def on_event(&handler)
10
+ @handler = handler
11
+ end
12
+
13
+ def start!
14
+ @queue.subscribe do |header, json_payload|
15
+ message = Message.new ({
16
+ :header => header,
17
+ :payload => JSON.parse(json_payload),
18
+ :event => @event_name,
19
+ :started_at => Time.now.to_i
20
+ })
21
+ @handler.call(message)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ module Pipelines
2
+ class BatchEventSource
3
+ def initialize(em, events)
4
+ @em, @events = em, events
5
+ end
6
+
7
+ def on_event(&handler)
8
+ @handler = handler
9
+ end
10
+
11
+ def on_batch_finished(&batch_finished_handler)
12
+ @batch_finished_handler = batch_finished_handler
13
+ end
14
+
15
+ def start!
16
+ @finalised = []
17
+ check_if_finished
18
+
19
+ message_finished = lambda do |m|
20
+ @finalised << m
21
+ check_if_finished
22
+ end
23
+
24
+ @events.each do |e|
25
+ message = Message.new({:payload => e})
26
+
27
+ message.on_rejected_broken(message_finished)
28
+ message.on_rejected(message_finished)
29
+ message.on_consumed(message_finished)
30
+
31
+ @handler.call(message)
32
+ end
33
+ end
34
+
35
+ private
36
+ def check_if_finished
37
+ finished = (@finalised.size == @events.size)
38
+
39
+ if finished and @batch_finished_handler
40
+ @em.next_tick { @batch_finished_handler.call(@finalised) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module Pipelines
2
+ class EventPipeline
3
+ def initialize(source, pipeline, monitoring)
4
+ @source, @pipeline, @monitoring = source, pipeline, monitoring
5
+
6
+ @source.on_event do |event_data|
7
+ @pipeline.notify(event_data)
8
+ end
9
+ end
10
+
11
+ def start!
12
+ @source.start!
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Pipelines
2
+ class ListEventSource
3
+ def initialize(events)
4
+ @events = events
5
+ end
6
+
7
+ def start!
8
+ @events.each do |e|
9
+ @handler.call({:payload => e})
10
+ end
11
+ end
12
+
13
+ def on_event(&handler)
14
+ @handler = handler
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ module Pipelines
2
+ class Message
3
+ def initialize(base_hash={})
4
+ hash!(base_hash)
5
+ created!
6
+ end
7
+
8
+ def [](key)
9
+ hash[key]
10
+ end
11
+
12
+ def []=(key, value)
13
+ check_if_mutation_allowed
14
+ hash[key] = value
15
+ end
16
+
17
+ def delete(key)
18
+ check_if_mutation_allowed
19
+ hash.delete key
20
+ end
21
+
22
+ def merge(other_hash)
23
+ check_if_mutation_allowed
24
+ Message.new(hash.merge(other_hash))
25
+ end
26
+
27
+ def on_consumed(callback=nil, &callback_block)
28
+ @consumed_callback = block_given? ? callback_block : callback
29
+ end
30
+
31
+ def on_rejected(callback=nil, &callback_block)
32
+ @rejected_callback = block_given? ? callback_block : callback
33
+ end
34
+
35
+ def on_rejected_broken(callback=nil, &callback_block)
36
+ @rejected_broken_callback = block_given? ? callback_block : callback
37
+ end
38
+
39
+ def consumed!
40
+ check_if_mutation_allowed
41
+ @state = :consumed
42
+ invoke(@consumed_callback)
43
+ end
44
+
45
+ def rejected!
46
+ check_if_mutation_allowed
47
+ @state = :rejected
48
+ invoke(@rejected_callback)
49
+ end
50
+
51
+ def broken!
52
+ check_if_mutation_allowed
53
+ @state = :rejected_broken
54
+ invoke(@rejected_broken_callback)
55
+ end
56
+
57
+ def hash
58
+ @backing_hash
59
+ end
60
+
61
+ def to_s
62
+ "#{self.class.name} state:#{@state} hash:#{hash}"
63
+ end
64
+
65
+ private
66
+
67
+ def hash!(other)
68
+ @backing_hash = symbolised(other)
69
+ end
70
+
71
+ def symbolised(raw_hash)
72
+ raw_hash.reduce({}) do |acc, (key, value)|
73
+ acc[key.to_s.to_sym] = value.is_a?(Hash) ? symbolised(value) : value
74
+ acc
75
+ end
76
+ end
77
+
78
+ def created!
79
+ @state = :created
80
+ end
81
+
82
+ def check_if_mutation_allowed
83
+ raise "Cannot mutate #{self}" unless @state == :created
84
+ end
85
+
86
+ def invoke(callback)
87
+ callback.call(self) if callback
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,24 @@
1
+ module Pipelines
2
+ class PeriodicEventSource
3
+ def initialize(em, interval_in_secs, &event_sourcing_code)
4
+ @em, @interval_in_secs, @event_sourcing_code = em, interval_in_secs, event_sourcing_code
5
+ end
6
+
7
+ def start!
8
+ event_sourcing_code = @event_sourcing_code
9
+
10
+ @em.add_periodic_timer(@interval_in_secs) do
11
+ tick!
12
+ end
13
+ end
14
+
15
+ def on_event(&handler)
16
+ @handler = handler
17
+ end
18
+
19
+ def tick!
20
+ event = @event_sourcing_code.call
21
+ @handler.call(event) if event
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ module Pipelines
2
+ class Pipeline
3
+ class TerminatorStage
4
+ def self.notify(ignored, also_ignored = {})
5
+ #noop
6
+ end
7
+ end
8
+
9
+ def initialize(spawner, context, monitoring, logger)
10
+ @spawner = spawner
11
+ @logger = logger
12
+ @context = context
13
+ @monitoring = monitoring
14
+ end
15
+
16
+ def for(event_definition)
17
+ stages = event_definition.map(&instantiate_with_dependencies)
18
+
19
+ monitoring = @monitoring
20
+ logger = @logger
21
+
22
+ first_stage_process = stages.reverse.reduce(TerminatorStage) do |current_head, next_stage|
23
+ @spawner.spawn do |input|
24
+ begin
25
+ logger.debug "#{next_stage.class}#notify with #{input}}"
26
+ next_stage.call(input) do |output|
27
+ current_head.notify(output)
28
+ end
29
+ rescue => exception
30
+ monitoring.inform_exception!(exception, next_stage)
31
+ end
32
+ end
33
+ end
34
+
35
+ @logger.info "Pipeline for event_definition is: #{stages.map(&:class).join('->')}"
36
+ first_stage_process
37
+ end
38
+
39
+ private
40
+ def instantiate_with_dependencies
41
+ lambda do |stage_class|
42
+ stage_instance = stage_class.new
43
+ @context.each do |name, value|
44
+ stage_instance.define_singleton_method(name) { value }
45
+ end
46
+ stage_instance
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module empipelines
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,36 @@
1
+ require 'empipelines/amqp_event_source'
2
+
3
+ module Pipelines
4
+ class StubQueue
5
+ def subscribe(&code)
6
+ @code = code
7
+ end
8
+
9
+ def publish(header, payload)
10
+ @code.call(header, payload)
11
+ end
12
+ end
13
+
14
+ describe AmqpEventSource do
15
+ it "adds payload, time and event type to message and sends to listener" do
16
+ json_payload = '{"key":"value"}'
17
+ queue = StubQueue.new
18
+ event_type = "NuclearWar"
19
+ header = stub('header')
20
+
21
+ received_messages = []
22
+
23
+ amqp_source = AmqpEventSource.new(queue, event_type)
24
+ amqp_source.on_event { |e| received_messages << e }
25
+ amqp_source.start!
26
+
27
+ queue.publish(header, json_payload)
28
+
29
+ received_messages.size.should eql(1)
30
+ received_messages.first[:header].should ==(header)
31
+ received_messages.first[:payload].should ==({:key => "value"})
32
+ received_messages.first[:event].should ==(event_type)
33
+ received_messages.first[:started_at].should_not be_nil
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ require 'empipelines/batch_event_source'
2
+
3
+ module Pipelines
4
+ describe BatchEventSource do
5
+
6
+ let (:em) do
7
+ em = mock('eventmachine')
8
+ em.stub(:next_tick).and_yield
9
+ em
10
+ end
11
+
12
+ it "sends each element on the list as a payload to the listener" do
13
+ events = [1,2,3,4,5,6,7,8,9,10]
14
+ source = BatchEventSource.new(em, events)
15
+
16
+ received = []
17
+ source.on_event do |e|
18
+ received << e
19
+ end
20
+
21
+ source.start!
22
+
23
+ received.map{ |i| i[:payload] }.should ==(events)
24
+ end
25
+
26
+ it "calls the batch finished callback when all items were processed" do
27
+ events = [1,2,3,4,5,6,7,8,9,10]
28
+ source = BatchEventSource.new(em, events)
29
+
30
+ has_finished = []
31
+
32
+ source.on_batch_finished do |messages|
33
+ has_finished << messages
34
+ end
35
+
36
+ source.on_event do |e|
37
+ e.consumed!
38
+ end
39
+
40
+ source.start!
41
+
42
+ has_finished.first.map{ |i| i[:payload] }.should ==(events)
43
+ end
44
+
45
+ it "finishes straight away if there are no events to process" do
46
+ source = BatchEventSource.new(em, [])
47
+
48
+ has_finished = []
49
+ source.on_batch_finished do |messages|
50
+ has_finished << true
51
+ end
52
+
53
+ source.on_event do |e|
54
+ raise 'should not be called!'
55
+ end
56
+
57
+ source.start!
58
+
59
+ has_finished.first.should be_true
60
+ end
61
+
62
+ it "does not call the batch finished callback if not all of the items were processed" do
63
+ events = [1,2,3,4,5,6,7,8,9,10]
64
+ source = BatchEventSource.new(em, events)
65
+
66
+ source.on_batch_finished do |messages|
67
+ raise "should not be called"
68
+ end
69
+
70
+ count = 0
71
+ source.on_event do |e|
72
+ e.consumed! if (count=+1) > 1
73
+ end
74
+
75
+ source.start!
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ require 'empipelines/event_pipeline'
2
+
3
+ module Pipelines
4
+ class StubSource
5
+ def initialize(event_data)
6
+ @event_data = event_data
7
+ end
8
+
9
+ def start!
10
+ @handler.call(@event_data)
11
+ end
12
+
13
+ def on_event(&event_handler)
14
+ @handler = event_handler
15
+ end
16
+ end
17
+
18
+ describe EventPipeline do
19
+ it "binds a source to a pipeline" do
20
+ monitoring = stub(:increment => nil)
21
+ event = stub('event')
22
+ pipeline = stub('processing pipeline')
23
+ source = StubSource.new(event)
24
+
25
+ pipeline.should_receive(:notify).with(event)
26
+
27
+ event_pipeline = EventPipeline.new(source, pipeline, monitoring)
28
+ event_pipeline.start!
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ require 'empipelines/list_event_source'
2
+
3
+ module Pipelines
4
+ describe ListEventSource do
5
+ it 'sends each element of the list to the handler' do
6
+ items = [1, 2, 3, 4, 5, 6]
7
+ expected_messages = items.map { |i| {:payload => i} }
8
+ received_messages = []
9
+
10
+ source = ListEventSource.new(items)
11
+ source.on_event { |msg| received_messages << msg}
12
+ source.start!
13
+
14
+ received_messages.should eql(expected_messages)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,138 @@
1
+ require 'empipelines/message'
2
+
3
+ module Pipelines
4
+ describe Message do
5
+ context "mostly behaves like a hashmap" do
6
+ it "stores values under symbolised keys" do
7
+ original_hash = {:a => 1, :b => 2}
8
+
9
+ message = Message.new(original_hash)
10
+
11
+ message[:a].should ==(original_hash[:a])
12
+ message[:b].should ==(original_hash[:b])
13
+ message[:doesntexist].should ==(original_hash[:doesntexist])
14
+ end
15
+
16
+ it "symbolises keys of all maps in the message" do
17
+ message = Message.new({
18
+ :a => 1,
19
+ "b" => 2,
20
+ 3 => 3 ,
21
+ "d" => {
22
+ "d1" => {"e" => 5},
23
+ "d2" => nil}
24
+ })
25
+ message[:a].should ==(1)
26
+ message[:b].should ==(2)
27
+ message["3".to_sym].should ==(3)
28
+ message[:d][:d1][:e].should ==(5)
29
+ message[:d][:d2].should be_nil
30
+ end
31
+
32
+ it "allows for values to be CRUD" do
33
+ original_hash = {:a => 1, :b => 2, :c => 0}
34
+
35
+ message = Message.new(original_hash)
36
+
37
+ message[:a] = 666
38
+ message.delete :b
39
+ message[:z] = 999
40
+
41
+ message[:a].should ==(666)
42
+ message[:b].should be_nil
43
+ message[:c].should ==(original_hash[:c])
44
+ message[:z].should ==(999)
45
+ end
46
+
47
+ it "can be merged with a map, symbolising keys" do
48
+ original = Message.new({'a' => 1})
49
+ merged = original.merge({'b' => 2})
50
+
51
+ original[:b].should be_nil
52
+
53
+ merged[:a].should ==(original[:a])
54
+ merged[:b].should_not be_nil
55
+ end
56
+ end
57
+
58
+ context "message status" do
59
+
60
+ let (:handler_that_should_never_be_called) { lambda { raise "This shouldnt happen" } }
61
+
62
+ it "doesnt do anything if no state callback specified" do
63
+ Message.new.consumed!
64
+ Message.new.rejected!
65
+ Message.new.broken!
66
+ end
67
+
68
+ it "is possible to reject a message if broken"do
69
+ called = []
70
+
71
+ message = Message.new
72
+ message.on_rejected_broken do |msg|
73
+ called << msg
74
+ end
75
+
76
+ message.on_rejected(handler_that_should_never_be_called)
77
+ message.on_consumed(handler_that_should_never_be_called)
78
+
79
+ message.broken!
80
+
81
+ called.should==([message])
82
+ end
83
+
84
+ it "is possible to reject a message if consumer cant handle it" do
85
+ called = []
86
+
87
+ message = Message.new
88
+ message.on_rejected do |msg|
89
+ called << msg
90
+ end
91
+
92
+ message.on_rejected_broken(handler_that_should_never_be_called)
93
+ message.on_consumed(handler_that_should_never_be_called)
94
+
95
+ message.rejected!
96
+
97
+ called.should==([message])
98
+ end
99
+
100
+ it "is possible to mark a message as consumed" do
101
+ called = []
102
+
103
+ message = Message.new
104
+ message.on_consumed do |msg|
105
+ called << msg
106
+ end
107
+
108
+ message.on_rejected_broken(handler_that_should_never_be_called)
109
+ message.on_rejected(handler_that_should_never_be_called)
110
+
111
+ message.consumed!
112
+
113
+ called.should==([message])
114
+ end
115
+
116
+ it "is not possible to change a message after marking as consumed or rejected" do
117
+ read = lambda { |m| m[:some_key] }
118
+ mutate = lambda { |m| m[:some_key] = :some_value }
119
+
120
+ consumed = Message.new
121
+ consumed.consumed!
122
+
123
+ rejected = Message.new
124
+ rejected.rejected!
125
+
126
+ broken = Message.new
127
+ broken.broken!
128
+
129
+ lambda{ read.call(consumed) }.should_not raise_error
130
+ lambda{ mutate.call(consumed) }.should raise_error
131
+ lambda{ read.call(rejected) }.should_not raise_error
132
+ lambda{ mutate.call(rejected) }.should raise_error
133
+ lambda{ read.call(broken) }.should_not raise_error
134
+ lambda{ mutate.call(broken) }.should raise_error
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,28 @@
1
+ require 'empipelines/periodic_event_source'
2
+
3
+ module Pipelines
4
+ describe PeriodicEventSource do
5
+ it 'schedules itself with eventmachine' do
6
+ em = stub('eventmachine')
7
+ em.should_receive(:add_periodic_timer).with(5)
8
+ PeriodicEventSource.new(em, 5){ "something cool!" }.start!
9
+ end
10
+
11
+ it 'sends the result of the periodic action to the handler' do
12
+ expected_message = { :something => "goes here" }
13
+ received_messages = []
14
+
15
+ source = PeriodicEventSource.new(stub('eventmachine'), 666){ expected_message }
16
+ source.on_event { |msg| received_messages << msg}
17
+ source.tick!
18
+
19
+ received_messages.should eql([expected_message])
20
+ end
21
+
22
+ it 'doesnt o anything if no event was generated' do
23
+ source = PeriodicEventSource.new(stub('eventmachine'), 666){ nil }
24
+ source.on_event { |m| raise "should not be called" }
25
+ source.tick!
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,106 @@
1
+ require "empipelines/pipeline"
2
+
3
+ module Pipelines
4
+ class AddOne
5
+ def call(input, &next_stage)
6
+ next_stage.call({:data => (input[:data] + 1)})
7
+ end
8
+ end
9
+
10
+ class SquareIt
11
+ def call(input, &next_stage)
12
+ next_stage.call({:data => (input[:data] * input[:data])})
13
+ end
14
+ end
15
+
16
+ class BrokenStage
17
+ def call(ignore, &ignored_too)
18
+ raise "Boo!"
19
+ end
20
+ end
21
+
22
+ class DeadEnd
23
+ def call(input, &also_ignore)
24
+ #noop
25
+ end
26
+ end
27
+
28
+ class NeedsAnApple
29
+ def call(input, &next_stage)
30
+ next_stage.call(input.merge({:apple => apple}))
31
+ end
32
+ end
33
+
34
+ class NeedsAnOrange
35
+ def call(input, &next_stage)
36
+ next_stage.call(input.merge({:orange => orange}))
37
+ end
38
+ end
39
+
40
+ class GlobalHolder
41
+ @@value = nil
42
+ def GlobalHolder.held
43
+ @@value
44
+ end
45
+
46
+ def initialize
47
+ @@value = nil
48
+ end
49
+
50
+ def call(input, &next_step)
51
+ @@value = input
52
+ next_step.call(self.class)
53
+ end
54
+ end
55
+
56
+ class StubSpawner
57
+
58
+ class StubProcess
59
+ def initialize(block)
60
+ @block = block
61
+ end
62
+
63
+ def notify(input)
64
+ @block.call(input)
65
+ end
66
+ end
67
+
68
+ def spawn(&block)
69
+ StubProcess.new(block)
70
+ end
71
+ end
72
+
73
+ describe Pipeline do
74
+ let(:logger) {stub(:info => true, :debug => true)}
75
+ it "chains the actions using processes" do
76
+ event_chain = [AddOne, SquareIt, GlobalHolder]
77
+ pipelines = Pipeline.new(StubSpawner.new, {}, stub('monitoring'), logger)
78
+ pipeline = pipelines.for(event_chain)
79
+ pipeline.notify({:data =>1})
80
+ GlobalHolder.held.should eql({:data => 4})
81
+ end
82
+
83
+ it "does not send to the next if last returned nil" do
84
+ event_chain = [AddOne, SquareIt, DeadEnd, GlobalHolder]
85
+ pipelines = Pipeline.new(StubSpawner.new, {}, stub('monitoring'), logger)
86
+ pipeline = pipelines.for(event_chain)
87
+ pipeline.notify({:data => 1})
88
+ GlobalHolder.held.should be_nil
89
+ end
90
+
91
+ it "makes all objects in the context object available to stages" do
92
+ event_chain = [NeedsAnApple, NeedsAnOrange, GlobalHolder]
93
+ pipelines = Pipeline.new(StubSpawner.new, {:apple => :some_apple, :orange => :some_orange}, stub('monitoring'), logger)
94
+ pipeline = pipelines.for(event_chain)
95
+ pipeline.notify({})
96
+ GlobalHolder.held.should eql({:apple => :some_apple, :orange => :some_orange})
97
+ end
98
+
99
+ it "sends exception to the proper handler" do
100
+ monitoring = mock()
101
+ monitoring.should_receive(:inform_exception!)
102
+ pipeline = Pipeline.new(StubSpawner.new, {}, monitoring, logger)
103
+ pipeline.for([BrokenStage]).notify({})
104
+ end
105
+ end
106
+ end
File without changes
@@ -0,0 +1,54 @@
1
+ def request(path = path)
2
+ responses = []
3
+ described_class.request(path) { |response| responses << response }
4
+ responses
5
+ end
6
+
7
+ def process(message = message, stage_class = described_class)
8
+ messages = []
9
+ stage_instance = stage_class.new
10
+
11
+ #TODO: should this be here? Is there an easy way?
12
+ context.each {|k,v| stage_instance.define_singleton_method(k) {v} }
13
+
14
+ stage_instance.call(message) { |message| messages << message }
15
+ messages
16
+ end
17
+
18
+ RSpec::Matchers.define :return_response do |expected|
19
+ match do |responses|
20
+ responses.include?(expected)
21
+ end
22
+ end
23
+
24
+ RSpec::Matchers.define :return_any_response do |expected|
25
+ match do |responses|
26
+ responses.any?
27
+ end
28
+ end
29
+
30
+ RSpec::Matchers.define :send_messages_including do |expected|
31
+ match do |messages|
32
+ messages.any? do |message|
33
+ expected.all? do |key, value|
34
+ message[key] == value
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ RSpec::Matchers.define :send_some_message do |expected|
41
+ match do |messages|
42
+ messages.any?
43
+ end
44
+ end
45
+
46
+ RSpec::Matchers.define :send_no_message do |expected|
47
+ match do |messages|
48
+ messages.empty?
49
+ end
50
+ end
51
+
52
+ RSpec::Matchers.define :send_message do |expected|
53
+ match { |messages| messages.include?(expected) }
54
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: empipelines
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tobias Schmidt
9
+ - Phil Calcado
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-12-19 00:00:00.000000000Z
14
+ dependencies: []
15
+ description: Simple Event Handling Pipeline Architecture for EventMachine
16
+ email: pcalcado+empipelines@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - README.md
21
+ files:
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - README.md
25
+ - Rakefile
26
+ - empipelines.gemspec
27
+ - lib/empipelines.rb
28
+ - lib/empipelines/amqp_event_source.rb
29
+ - lib/empipelines/batch_event_source.rb
30
+ - lib/empipelines/event_pipeline.rb
31
+ - lib/empipelines/list_event_source.rb
32
+ - lib/empipelines/message.rb
33
+ - lib/empipelines/periodic_event_source.rb
34
+ - lib/empipelines/pipeline.rb
35
+ - spec/empipelines/amqp_event_source_spec.rb
36
+ - spec/empipelines/batch_event_source_spec.rb
37
+ - spec/empipelines/event_pipeline_spec.rb
38
+ - spec/empipelines/list_event_source_spec.rb
39
+ - spec/empipelines/message_spec.rb
40
+ - spec/empipelines/periodic_event_source_spec.rb
41
+ - spec/empipelines/pipeline_spec.rb
42
+ - spec/spec_helper.rb
43
+ - spec/stage_helper.rb
44
+ homepage: http://github.com/pcalcado/empipelines
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --charset=UTF-8
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ segments:
58
+ - 0
59
+ hash: -303490141068781308
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project: empipelines
68
+ rubygems_version: 1.8.10
69
+ signing_key:
70
+ specification_version: 2
71
+ summary: Simple Event Handling Pipeline Architecture for EventMachine
72
+ test_files: []