empipelines 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []