pheme 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b5feb32f5663e1118ccd4307e0552cb85e513a10
4
+ data.tar.gz: 80cad78d39a25a965129eedb7411cbc96ccffc6b
5
+ SHA512:
6
+ metadata.gz: 3348f6ea4828665392ce7770e577ba35fb4d07d7d5409c38a1dde176b9fc194a2e3315c9a9eef4f1445ef9c0875acd3ee62cb778233eb5758300ebbfcaeff581
7
+ data.tar.gz: 2d608692cb8dba727888309160fba22f84a05d2121ed00251186f0fa980ff4380aa696812cfa2fa7c411d1e1749bffe64a8a3b988f061aa513d8bf78827ee1a4
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalization:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ Gemfile.lock
32
+ .ruby-version
33
+ .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Wealthsimple
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # pheme [![Circle CI](https://circleci.com/gh/wealthsimple/pheme.svg?style=svg)](https://circleci.com/gh/wealthsimple/pheme)
2
+
3
+ Ruby SNS publisher + SQS poller & message handler
4
+
5
+ ## installation & config
6
+
7
+ ```ruby
8
+ # Gemfile
9
+ gem 'pheme'
10
+ ```
11
+
12
+ ```ruby
13
+ # Initializer
14
+ aws_config = {
15
+ credentials: Aws::Credentials.new('YOUR_ACCESS_KEY_ID', 'YOUR_SECRET_ACCESS_KEY'),
16
+ region: 'us-east-1',
17
+ }
18
+ AWS_SNS_CLIENT = Aws::SNS::Client.new(aws_config)
19
+ AWS_SQS_CLIENT = Aws::SQS::Client.new(aws_config)
20
+
21
+ Pheme.configure do |config|
22
+ config.sqs_client = AWS_SQS_CLIENT
23
+ config.sns_client = AWS_SNS_CLIENT
24
+ config.logger = Logger.new(STDOUT) # Optionally replace with your app logger, e.g. `Rails.logger`
25
+ end
26
+ ```
27
+
28
+ # usage
29
+
30
+ See https://github.com/wealthsimple/pheme/tree/master/spec/support for example implementations of each class.
31
+
32
+ TODO: write better usage instructions.
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ machine:
2
+ ruby:
3
+ version: 2.3.0
data/lib/pheme.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'active_support/all'
2
+ require 'recursive-open-struct'
3
+ require 'aws-sdk'
4
+ require 'securerandom'
5
+
6
+ require 'pheme/version'
7
+ require 'pheme/configuration'
8
+ require 'pheme/logger'
9
+ require 'pheme/topic_publisher'
10
+ require 'pheme/message_handler'
11
+ require 'pheme/queue_poller'
@@ -0,0 +1,34 @@
1
+ module Pheme
2
+ class << self
3
+ attr_writer :configuration
4
+ end
5
+
6
+ def self.configuration
7
+ @configuration ||= Configuration.new
8
+ end
9
+
10
+ def self.configure
11
+ yield(configuration)
12
+ end
13
+
14
+ def self.reset_configuration!
15
+ @configuration = Configuration.new
16
+ end
17
+
18
+ class Configuration
19
+ ATTRIBUTES = [:sns_client, :sqs_client, :logger]
20
+ attr_accessor *ATTRIBUTES
21
+
22
+ def initialize
23
+ @logger ||= Logger.new(STDOUT)
24
+ end
25
+
26
+ def validate!
27
+ ATTRIBUTES.each do |attribute|
28
+ raise "Invalid or missing configuration for #{attribute}" unless send(attribute).present?
29
+ end
30
+ raise "sns_client must be a Aws::SNS::Client" unless sns_client.is_a?(Aws::SNS::Client)
31
+ raise "sns_client must be a Aws::SQS::Client" unless sqs_client.is_a?(Aws::SQS::Client)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module Pheme
2
+ def self.log(method, text)
3
+ @logger ||= ActiveSupport::TaggedLogging.new(configuration.logger)
4
+ @tag ||= "pheme_#{SecureRandom.uuid}"
5
+ @logger.tagged(@tag) { @logger.send(method, text) }
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Pheme
2
+ class MessageHandler
3
+ attr_reader :message
4
+
5
+ def initialize(message:)
6
+ @message = message
7
+ end
8
+
9
+ def handle
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ module Pheme
2
+ class QueuePoller
3
+ attr_accessor :queue_url, :queue_poller, :connection_pool_block, :poller_configuration
4
+
5
+ def initialize(queue_url:, connection_pool_block: false, poller_configuration: {})
6
+ @queue_url = queue_url
7
+ @queue_poller = Aws::SQS::QueuePoller.new(queue_url)
8
+ @connection_pool_block = connection_pool_block
9
+ @poller_configuration = poller_configuration.merge({
10
+ wait_time_seconds: 10, # amount of time a long polling receive call can wait for a mesage before receiving a empty response (which will trigger another polling request)
11
+ idle_timeout: 20, # disconnects poller after 20 seconds of idle time
12
+ visibility_timeout: 30, # length of time in seconds that this message will not be visible to other receiving components
13
+ skip_delete: true, # manually delete messages
14
+ })
15
+ end
16
+
17
+ def poll
18
+ Pheme.log(:info, "Long-polling for messages on #{queue_url}")
19
+ with_optional_connection_pool_block do
20
+ queue_poller.poll(poller_configuration) do |message|
21
+ begin
22
+ handle(parse_message(message))
23
+ queue_poller.delete_message(message)
24
+ rescue => e
25
+ Pheme.log(:error, "Exception: #{e.inspect}")
26
+ Pheme.log(:error, e.backtrace.join("\n"))
27
+ end
28
+ end
29
+ end
30
+ Pheme.log(:info, "Finished long-polling after #{@poller_configuration[:idle_timeout]} seconds.")
31
+ end
32
+
33
+ def parse_message(message)
34
+ Pheme.log(:info, "Received JSON payload: #{message.body}")
35
+ body = JSON.parse(message.body)
36
+ parsed_body = JSON.parse(body['Message'])
37
+ RecursiveOpenStruct.new(parsed_body, recurse_over_arrays: true)
38
+ end
39
+
40
+ def handle(message)
41
+ raise NotImplementedError
42
+ end
43
+
44
+ private
45
+
46
+ def with_optional_connection_pool_block(&blk)
47
+ if connection_pool_block
48
+ ActiveRecord::Base.connection_pool.with_connection { blk.call }
49
+ else
50
+ blk.call
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ module Pheme
2
+ class TopicPublisher
3
+ attr_accessor :topic_arn
4
+
5
+ def initialize(topic_arn:)
6
+ @topic_arn = topic_arn
7
+ end
8
+
9
+ def publish_events
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def publish(message)
14
+ Pheme.log(:info, "Publishing to #{topic_arn}: #{message}")
15
+ Pheme.configuration.sns_client.publish(topic_arn: topic_arn, message: message.to_json)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Pheme
2
+ VERSION = "0.0.1"
3
+ end
data/pheme.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require "pheme/version"
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = "pheme"
9
+ gem.version = Pheme::VERSION
10
+ gem.authors = ["Peter Graham"]
11
+ gem.email = ["peter@wealthsimple.com"]
12
+ gem.description = %q{Ruby AWS SNS publisher + SQS poller & message handler}
13
+ gem.summary = %q{Ruby SNS publisher + SQS poller & message handler}
14
+ gem.homepage = "https://github.com/wealthsimple/pheme"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+ gem.licenses = ["MIT"]
21
+ gem.required_ruby_version = ">= 2.0.0"
22
+
23
+ gem.add_dependency "aws-sdk", "~> 2"
24
+ gem.add_dependency "activesupport", "~> 4"
25
+ gem.add_dependency "recursive-open-struct", "~> 1"
26
+
27
+ gem.add_development_dependency "rspec", "~> 3.4"
28
+ gem.add_development_dependency "rspec_junit_formatter", "~> 0.2"
29
+ end
@@ -0,0 +1,22 @@
1
+ describe Pheme do
2
+ describe ".configure" do
3
+ let(:sns_client) { double }
4
+ let(:sqs_client) { double }
5
+ let(:custom_logger) { double }
6
+ it "sets global configuration" do
7
+ expect(described_class.configuration.sns_client).to be_nil
8
+ expect(described_class.configuration.sqs_client).to be_nil
9
+ expect(described_class.configuration.logger).to be_a(Logger)
10
+
11
+ described_class.configure do |config|
12
+ config.sns_client = sns_client
13
+ config.sqs_client = sqs_client
14
+ config.logger = custom_logger
15
+ end
16
+
17
+ expect(described_class.configuration.sns_client).to eq(sns_client)
18
+ expect(described_class.configuration.sqs_client).to eq(sqs_client)
19
+ expect(described_class.configuration.logger).to eq(custom_logger)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ describe Pheme::MessageHandler do
2
+ before(:each) { use_default_configuration! }
3
+ let(:message) { RecursiveOpenStruct.new(status: "complete") }
4
+ subject { ExampleMessageHandler.new(message: message) }
5
+
6
+ describe "#handle" do
7
+ it "handles the message correctly" do
8
+ expect(Pheme).to receive(:log).with(:info, "Done")
9
+ subject.handle
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,111 @@
1
+ describe Pheme::QueuePoller do
2
+ let(:queue_url) { "https://sqs.us-east-1.amazonaws.com/whatever" }
3
+ let(:poller) do
4
+ poller = double
5
+ allow(poller).to receive(:poll).with(kind_of(Hash))
6
+ poller
7
+ end
8
+ before(:each) do
9
+ use_default_configuration!
10
+ allow(Aws::SQS::QueuePoller).to receive(:new) { poller }
11
+ end
12
+
13
+ describe "#poll" do
14
+ before(:each) do
15
+ module ActiveRecord
16
+ class Base
17
+ def self.connection_pool
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ context "with connection pool block" do
24
+ let(:mock_connection_pool) { double }
25
+
26
+ before(:each) do
27
+ allow(ActiveRecord::Base).to receive(:connection_pool) { mock_connection_pool }
28
+ allow(mock_connection_pool).to receive(:with_connection).and_yield
29
+ end
30
+
31
+ subject { ExampleQueuePoller.new(queue_url: queue_url, connection_pool_block: true) }
32
+
33
+ it "uses the connection pool block" do
34
+ expect(mock_connection_pool).to receive(:with_connection)
35
+ subject.poll
36
+ end
37
+ end
38
+
39
+ context "without connection pool block" do
40
+ subject { ExampleQueuePoller.new(queue_url: queue_url) }
41
+
42
+ it "does not call ActiveRecord" do
43
+ expect(ActiveRecord::Base).not_to receive(:connection_pool)
44
+ subject.poll
45
+ end
46
+ end
47
+
48
+ context "when a valid message is yielded" do
49
+ let(:message_body) do
50
+ {
51
+ id: "id-123",
52
+ status: "complete",
53
+ }
54
+ end
55
+ let(:message) do
56
+ message = double
57
+ allow(message).to receive(:body) do
58
+ {Message: message_body.to_json,}.to_json
59
+ end
60
+ message
61
+ end
62
+ before(:each) do
63
+ allow(poller).to receive(:poll).and_yield(message)
64
+ end
65
+
66
+ subject { ExampleQueuePoller.new(queue_url: queue_url) }
67
+
68
+ it "handles the message" do
69
+ expect(ExampleMessageHandler).to receive(:new).with(message: RecursiveOpenStruct.new(message_body))
70
+ subject.poll
71
+ end
72
+
73
+ it "deletes the message from the queue" do
74
+ expect(poller).to receive(:delete_message).with(message)
75
+ subject.poll
76
+ end
77
+ end
78
+
79
+ context "when an invalid message is yielded" do
80
+ let(:message_body) do
81
+ {
82
+ id: "id-123",
83
+ status: "unknown-abc",
84
+ }
85
+ end
86
+ let(:message) do
87
+ message = double
88
+ allow(message).to receive(:body) do
89
+ {Message: message_body.to_json}.to_json
90
+ end
91
+ message
92
+ end
93
+ before(:each) do
94
+ allow(poller).to receive(:poll).and_yield(message)
95
+ allow(Pheme).to receive(:log)
96
+ end
97
+
98
+ subject { ExampleQueuePoller.new(queue_url: queue_url) }
99
+
100
+ it "logs the error" do
101
+ subject.poll
102
+ expect(Pheme).to have_received(:log).with(:error, "Exception: #<ArgumentError: Unknown message status: unknown-abc>")
103
+ end
104
+
105
+ it "does not delete the message from the queue" do
106
+ expect(poller).not_to receive(:delete_message)
107
+ subject.poll
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,22 @@
1
+ require 'rspec'
2
+
3
+ require './lib/pheme'
4
+
5
+ Dir["./spec/support/**/*.rb"].each { |f| require f }
6
+
7
+ RSpec.configure do |config|
8
+ config.filter_run :focus
9
+ config.run_all_when_everything_filtered = true
10
+
11
+ config.before(:each) do
12
+ Pheme.reset_configuration!
13
+ end
14
+ end
15
+
16
+ def use_default_configuration!
17
+ Pheme.configure do |config|
18
+ config.sqs_client = double
19
+ config.sns_client = double
20
+ config.logger = Logger.new(nil)
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ class ExampleMessageHandler < Pheme::MessageHandler
2
+ def handle
3
+ case message.status
4
+ when "complete"
5
+ Pheme.log(:info, "Done")
6
+ when "rejected"
7
+ Pheme.log(:error, "Oops")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class ExamplePublisher < Pheme::TopicPublisher
2
+ def publish_events
3
+ 2.times do |n|
4
+ publish({
5
+ id: "id-#{n}",
6
+ status: "complete",
7
+ })
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class ExampleQueuePoller < Pheme::QueuePoller
2
+ def handle(message)
3
+ case message.status
4
+ when 'complete', 'rejected'
5
+ ExampleMessageHandler.new(message: message).handle
6
+ else
7
+ raise ArgumentError, "Unknown message status: #{message.status}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ describe Pheme::TopicPublisher do
2
+ before(:each) { use_default_configuration! }
3
+ subject { ExamplePublisher.new(topic_arn: "arn:aws:sns:whatever") }
4
+
5
+ describe "#publish_events" do
6
+ it "publishes the correct events" do
7
+ expect(Pheme.configuration.sns_client).to receive(:publish).with({
8
+ topic_arn: "arn:aws:sns:whatever",
9
+ message: {id: "id-0", status: "complete"}.to_json,
10
+ })
11
+ expect(Pheme.configuration.sns_client).to receive(:publish).with({
12
+ topic_arn: "arn:aws:sns:whatever",
13
+ message: {id: "id-1", status: "complete"}.to_json,
14
+ })
15
+ subject.publish_events
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ describe Pheme do
2
+ it "has a version" do
3
+ expect(described_class::VERSION).to be_a(String)
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pheme
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Peter Graham
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: recursive-open-struct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec_junit_formatter
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ description: Ruby AWS SNS publisher + SQS poller & message handler
84
+ email:
85
+ - peter@wealthsimple.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - Gemfile
93
+ - LICENSE
94
+ - README.md
95
+ - circle.yml
96
+ - lib/pheme.rb
97
+ - lib/pheme/configuration.rb
98
+ - lib/pheme/logger.rb
99
+ - lib/pheme/message_handler.rb
100
+ - lib/pheme/queue_poller.rb
101
+ - lib/pheme/topic_publisher.rb
102
+ - lib/pheme/version.rb
103
+ - pheme.gemspec
104
+ - spec/configuration_spec.rb
105
+ - spec/message_handler_spec.rb
106
+ - spec/queue_poller_spec.rb
107
+ - spec/spec_helper.rb
108
+ - spec/support/example_message_handler.rb
109
+ - spec/support/example_publisher.rb
110
+ - spec/support/example_queue_poller.rb
111
+ - spec/topic_publisher_spec.rb
112
+ - spec/version_spec.rb
113
+ homepage: https://github.com/wealthsimple/pheme
114
+ licenses:
115
+ - MIT
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 2.0.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 2.5.1
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Ruby SNS publisher + SQS poller & message handler
137
+ test_files:
138
+ - spec/configuration_spec.rb
139
+ - spec/message_handler_spec.rb
140
+ - spec/queue_poller_spec.rb
141
+ - spec/spec_helper.rb
142
+ - spec/support/example_message_handler.rb
143
+ - spec/support/example_publisher.rb
144
+ - spec/support/example_queue_poller.rb
145
+ - spec/topic_publisher_spec.rb
146
+ - spec/version_spec.rb