rosetta_queue 0.4.0
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/History.txt +38 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +11 -0
- data/Rakefile +39 -0
- data/VERSION.yml +4 -0
- data/cucumber.yml +1 -0
- data/examples/sample_amqp_consumer.rb +45 -0
- data/examples/sample_amqp_fanout_consumer.rb +52 -0
- data/examples/sample_amqp_fanout_producer.rb +18 -0
- data/examples/sample_amqp_producer.rb +16 -0
- data/features/filtering.feature +31 -0
- data/features/messaging.feature +48 -0
- data/features/step_definitions/common_messaging_steps.rb +82 -0
- data/features/step_definitions/filtering_steps.rb +17 -0
- data/features/step_definitions/point_to_point_steps.rb +22 -0
- data/features/step_definitions/publish_subscribe_steps.rb +25 -0
- data/features/support/env.rb +25 -0
- data/features/support/sample_consumers.rb +29 -0
- data/lib/rosetta_queue.rb +23 -0
- data/lib/rosetta_queue/adapter.rb +39 -0
- data/lib/rosetta_queue/adapters/amqp.rb +48 -0
- data/lib/rosetta_queue/adapters/amqp_evented.rb +132 -0
- data/lib/rosetta_queue/adapters/amqp_synch.rb +123 -0
- data/lib/rosetta_queue/adapters/base.rb +27 -0
- data/lib/rosetta_queue/adapters/beanstalk.rb +56 -0
- data/lib/rosetta_queue/adapters/fake.rb +26 -0
- data/lib/rosetta_queue/adapters/null.rb +57 -0
- data/lib/rosetta_queue/adapters/stomp.rb +88 -0
- data/lib/rosetta_queue/base.rb +15 -0
- data/lib/rosetta_queue/consumer.rb +30 -0
- data/lib/rosetta_queue/consumer_managers/base.rb +24 -0
- data/lib/rosetta_queue/consumer_managers/evented.rb +43 -0
- data/lib/rosetta_queue/consumer_managers/threaded.rb +94 -0
- data/lib/rosetta_queue/core_ext/string.rb +22 -0
- data/lib/rosetta_queue/core_ext/time.rb +20 -0
- data/lib/rosetta_queue/destinations.rb +33 -0
- data/lib/rosetta_queue/exception_handler.rb +105 -0
- data/lib/rosetta_queue/exceptions.rb +10 -0
- data/lib/rosetta_queue/filters.rb +58 -0
- data/lib/rosetta_queue/logger.rb +27 -0
- data/lib/rosetta_queue/message_handler.rb +52 -0
- data/lib/rosetta_queue/producer.rb +21 -0
- data/lib/rosetta_queue/spec_helpers.rb +5 -0
- data/lib/rosetta_queue/spec_helpers/hash.rb +21 -0
- data/lib/rosetta_queue/spec_helpers/helpers.rb +47 -0
- data/lib/rosetta_queue/spec_helpers/publishing_matchers.rb +144 -0
- data/spec/rosetta_queue/adapter_spec.rb +101 -0
- data/spec/rosetta_queue/adapters/amqp_synchronous_spec.rb +277 -0
- data/spec/rosetta_queue/adapters/beanstalk_spec.rb +47 -0
- data/spec/rosetta_queue/adapters/fake_spec.rb +72 -0
- data/spec/rosetta_queue/adapters/null_spec.rb +31 -0
- data/spec/rosetta_queue/adapters/shared_adapter_behavior.rb +38 -0
- data/spec/rosetta_queue/adapters/shared_fanout_behavior.rb +20 -0
- data/spec/rosetta_queue/adapters/stomp_spec.rb +126 -0
- data/spec/rosetta_queue/consumer_managers/evented_spec.rb +56 -0
- data/spec/rosetta_queue/consumer_managers/shared_manager_behavior.rb +26 -0
- data/spec/rosetta_queue/consumer_managers/threaded_spec.rb +51 -0
- data/spec/rosetta_queue/consumer_spec.rb +99 -0
- data/spec/rosetta_queue/core_ext/string_spec.rb +15 -0
- data/spec/rosetta_queue/destinations_spec.rb +34 -0
- data/spec/rosetta_queue/exception_handler_spec.rb +106 -0
- data/spec/rosetta_queue/filters_spec.rb +57 -0
- data/spec/rosetta_queue/message_handler_spec.rb +47 -0
- data/spec/rosetta_queue/producer_spec.rb +77 -0
- data/spec/rosetta_queue/shared_messaging_behavior.rb +21 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +47 -0
- metadata +142 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# Taken from ActiveSupport
|
2
|
+
class String
|
3
|
+
def camelize(first_letter_in_uppercase = true)
|
4
|
+
if first_letter_in_uppercase
|
5
|
+
self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
6
|
+
else
|
7
|
+
self.first.downcase + camelize(self)[1..-1]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def classify
|
12
|
+
camelize(self.sub(/.*\./, ''))
|
13
|
+
end
|
14
|
+
|
15
|
+
def underscore
|
16
|
+
self.gsub(/::/, '/').
|
17
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
18
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
19
|
+
tr("-", "_").
|
20
|
+
downcase
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Time
|
4
|
+
|
5
|
+
DATE_FORMATS = {
|
6
|
+
:db => "%Y-%m-%d %H:%M:%S",
|
7
|
+
:number => "%Y%m%d%H%M%S",
|
8
|
+
:time => "%H:%M",
|
9
|
+
:short => "%d %b %H:%M",
|
10
|
+
:long => "%B %d, %Y %H:%M",
|
11
|
+
:long_ordinal => lambda { |time| time.strftime("%B #{time.day.ordinalize}, %Y %H:%M") },
|
12
|
+
:rfc822 => lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S #{time.formatted_offset(false)}") }
|
13
|
+
}
|
14
|
+
|
15
|
+
def to_formatted_s(format = :default)
|
16
|
+
return to_default_s unless formatter = DATE_FORMATS[format]
|
17
|
+
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
|
3
|
+
class Destinations
|
4
|
+
|
5
|
+
@dest = {}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def define
|
10
|
+
yield self
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear
|
14
|
+
@dest.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup(dest_name)
|
18
|
+
mapping = @dest[dest_name.to_sym]
|
19
|
+
raise "No destination mapping for '#{dest_name}' has been defined!" unless mapping
|
20
|
+
return mapping
|
21
|
+
end
|
22
|
+
|
23
|
+
def map(key, dest)
|
24
|
+
@dest[key] = dest
|
25
|
+
end
|
26
|
+
|
27
|
+
def queue_names
|
28
|
+
@dest.values
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# == ExceptionHandler
|
2
|
+
#
|
3
|
+
# RQ allows you to register exception handlers for different parts of messaging.
|
4
|
+
# The handlers can be a class with a ::handle method or simply a block.
|
5
|
+
# In both cases the handler needs to accept as arguments the exception and
|
6
|
+
# an additional info hash which contains useful information about the context
|
7
|
+
# in which the error was raised (i.e. the message involved).
|
8
|
+
#
|
9
|
+
# An example of class exception handler:
|
10
|
+
#
|
11
|
+
# class MessagingExceptionHandler
|
12
|
+
# def self.handle(exception, info)
|
13
|
+
# RosettaQueue.logger.error("An exception occurred when #{info[:action]} to/from #{info[:destination]}: \n#{e.message}\n#{e.backtrace.join("\n")}")
|
14
|
+
# RosettaQueue.logger.error("Message that caused exception:\n #{info[:message]}")
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# You can register it like so:
|
19
|
+
#
|
20
|
+
# RosettaQueue::ExceptionHandler.register(:all, MessagingExceptionHandler)
|
21
|
+
#
|
22
|
+
#
|
23
|
+
# Instead of :all you can specify the :consuming or :publishing action.
|
24
|
+
#
|
25
|
+
# Or you can register a block like so:
|
26
|
+
#
|
27
|
+
# RosettaQueue::ExceptionHandler.register(:all) do |exception, info|
|
28
|
+
# # ....
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# == Define DSL
|
32
|
+
#
|
33
|
+
# Like the other parts of RQ you can configure it with ::define (instead of using the
|
34
|
+
# ::register call). Here is an example of that using the block handler:
|
35
|
+
#
|
36
|
+
# RosettaQueue::ExceptionHandler.define do |handler|
|
37
|
+
# handler.for(:all) do |exception, info|
|
38
|
+
# case exception
|
39
|
+
# when SomeSpecificError
|
40
|
+
# #.....
|
41
|
+
# else
|
42
|
+
# #...
|
43
|
+
# #...
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# handler.for(:consuming) do |exception, info|
|
48
|
+
# # this will be called just for consuming errors in
|
49
|
+
# # addition to the :all one above
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
|
54
|
+
|
55
|
+
module RosettaQueue
|
56
|
+
class ExceptionHandler
|
57
|
+
class << self
|
58
|
+
|
59
|
+
def handle(messaging_action=:all, info_or_proc={})
|
60
|
+
yield
|
61
|
+
rescue Exception => e
|
62
|
+
handlers = handlers_for(messaging_action)
|
63
|
+
raise e if handlers.empty?
|
64
|
+
info = info_or_proc.respond_to?(:call) ? info_or_proc.call : info_or_proc
|
65
|
+
handlers.each { |h| h.handle(e, info) }
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def define
|
70
|
+
yield self
|
71
|
+
end
|
72
|
+
|
73
|
+
def reset_handlers
|
74
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
75
|
+
end
|
76
|
+
|
77
|
+
def register(messaging_action, handler_klass=nil, &block)
|
78
|
+
handler = handler_klass
|
79
|
+
if block_given?
|
80
|
+
def block.handle(*args)
|
81
|
+
call(*args)
|
82
|
+
end
|
83
|
+
handler = block
|
84
|
+
end
|
85
|
+
|
86
|
+
handlers[messaging_action] << handler
|
87
|
+
end
|
88
|
+
|
89
|
+
alias for register
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def handlers
|
94
|
+
@handlers || reset_handlers
|
95
|
+
end
|
96
|
+
|
97
|
+
def handlers_for(messaging_action)
|
98
|
+
return handlers[:all] if messaging_action == :all
|
99
|
+
|
100
|
+
handlers[messaging_action] + handlers[:all]
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
|
3
|
+
class RosettaQueueError < StandardError; end
|
4
|
+
class DestinationNotFound < RosettaQueueError; end
|
5
|
+
class RosettaQueueVariableNotFound < RosettaQueueError; end
|
6
|
+
class CallbackNotImplemented < RosettaQueueError; end
|
7
|
+
class AdapterException < RosettaQueueError; end
|
8
|
+
class StopProcessingException < Interrupt; end
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Example:
|
2
|
+
# RosettaQueue::Filters.define do |filter_for|
|
3
|
+
# filter_for.receiving { |message| JSON.parse(message) }
|
4
|
+
# filter_for.sending { |hash| hash.to_json }
|
5
|
+
# end
|
6
|
+
|
7
|
+
|
8
|
+
module RosettaQueue
|
9
|
+
class Filters
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def define
|
14
|
+
yield self
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset
|
18
|
+
@receiving = nil
|
19
|
+
@sending = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def receiving(&receiving_filter)
|
23
|
+
@receiving = receiving_filter
|
24
|
+
end
|
25
|
+
|
26
|
+
def sending(&sending_filter)
|
27
|
+
@sending = sending_filter
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_sending(message)
|
31
|
+
return message unless @sending
|
32
|
+
@sending.call(message)
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_receiving(message)
|
36
|
+
return message unless @receiving
|
37
|
+
@receiving.call(message)
|
38
|
+
end
|
39
|
+
|
40
|
+
def safe_process_sending(message)
|
41
|
+
safe(:process_sending, message)
|
42
|
+
end
|
43
|
+
|
44
|
+
def safe_process_receiving(message)
|
45
|
+
safe(:process_receiving, message)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def safe(filter_call, message)
|
51
|
+
send(filter_call)
|
52
|
+
rescue StandardError
|
53
|
+
message
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module RosettaQueue
|
4
|
+
class MissingLogger < ::StandardError; end
|
5
|
+
|
6
|
+
def self.logger=(new_logger)
|
7
|
+
@logger = new_logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.logger
|
11
|
+
return @logger if @logger
|
12
|
+
raise MissingLogger, "No logger has been set for RosettaQueue. Please define one with RosettaQueue.logger=."
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module RosettaQueue
|
18
|
+
|
19
|
+
class Logger < ::Logger
|
20
|
+
|
21
|
+
def format_message(severity, timestamp, progname, msg)
|
22
|
+
"[#{timestamp.to_formatted_s(:db)}] #{severity} -- : #{msg}\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
module MessageHandler
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
attr_reader :destination, :options_hash
|
6
|
+
|
7
|
+
def options(options = {})
|
8
|
+
@options_hash = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def publishes_to(destination)
|
12
|
+
@destination = destination
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribes_to(destination)
|
16
|
+
@destination = destination
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.included(receiver)
|
21
|
+
receiver.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :adapter_proxy
|
25
|
+
|
26
|
+
def destination
|
27
|
+
self.class.destination
|
28
|
+
end
|
29
|
+
|
30
|
+
def options_hash
|
31
|
+
self.class.options_hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_message(unfiltered_message)
|
35
|
+
ExceptionHandler::handle(:publishing,
|
36
|
+
lambda {
|
37
|
+
{ :message => Filters.safe_process_receiving(unfiltered_message),
|
38
|
+
:destination => destination,
|
39
|
+
:action => :consuming,
|
40
|
+
:options => options_hash
|
41
|
+
}
|
42
|
+
} ) do
|
43
|
+
on_message(Filters.process_receiving(unfiltered_message))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ack
|
48
|
+
adapter_proxy.ack unless adapter_proxy.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
|
3
|
+
class Producer < Base
|
4
|
+
include MessageHandler
|
5
|
+
|
6
|
+
def self.publish(destination, message, options = {})
|
7
|
+
ExceptionHandler::handle(:publishing,
|
8
|
+
lambda {
|
9
|
+
{:message => Filters.safe_process_sending(message),
|
10
|
+
:action => :publishing,
|
11
|
+
:destination => destination,
|
12
|
+
:options => options}
|
13
|
+
}) do
|
14
|
+
RosettaQueue::Adapter.instance.send_message(
|
15
|
+
Destinations.lookup(destination),
|
16
|
+
Filters.process_sending(message),
|
17
|
+
options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Hash
|
2
|
+
# To be used in conjuction with rspec's predicate matcher.
|
3
|
+
#
|
4
|
+
# For example, in story/feature or a functional spec you could say:
|
5
|
+
#
|
6
|
+
# expected_message = {'name' => 'Advertiser'}
|
7
|
+
# expected_message.should be_published_to(:advertiser_create)
|
8
|
+
#
|
9
|
+
def published_to?(destination)
|
10
|
+
received_message = nil
|
11
|
+
begin
|
12
|
+
Timeout::timeout(2) { received_message = RosettaQueue::Consumer.receive(destination)}
|
13
|
+
rescue Timeout::Error
|
14
|
+
raise "#{destination} should have received a message but did not NOTE: make sure there are no other processes which are polling messages"
|
15
|
+
end
|
16
|
+
|
17
|
+
# calling should == is kinda wierd, I know.. but in order to get a decent error message it is needed
|
18
|
+
received_message.should == self
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
# Adds helpful methods when doing application level testing.
|
3
|
+
# If you are using cucumber just include it in your World in the env.rb file:
|
4
|
+
# World {|world| world.extend RosettaQueue::SpecHelpers }
|
5
|
+
module SpecHelpers
|
6
|
+
require 'open-uri'
|
7
|
+
|
8
|
+
# *Currently* only works with ActiveMQ being used as gateway.
|
9
|
+
# This will clear the queues defined in the RosettaQueue::Destinations mapping.
|
10
|
+
# TODO: Figure out a better spot for this to allow for other gateways...
|
11
|
+
def clear_queues
|
12
|
+
RosettaQueue::Destinations.queue_names.each do |name|
|
13
|
+
queue = name.gsub('/queue/','')
|
14
|
+
open("http://127.0.0.1:8161/admin/deleteDestination.action?JMSDestination=#{queue}&JMSDestinationType=queue")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Publishes a given hash as json to the specified destination.
|
19
|
+
# Example:
|
20
|
+
# publish_message(expected_message, :to => :client_status, :options => {...})
|
21
|
+
# The :options will be passed to the publisher and are optional.
|
22
|
+
def publish_message(message, options)
|
23
|
+
options[:options] ||= {:persistent => false}
|
24
|
+
RosettaQueue::Producer.publish(options[:to], message, options[:options])
|
25
|
+
end
|
26
|
+
|
27
|
+
# Consumes the first message on queue of consumer that is passed in and uses the consumer to handle it.
|
28
|
+
# Example:
|
29
|
+
# consume_once_with ClientStatusConsumer
|
30
|
+
def consume_once_with(consumer)
|
31
|
+
consumer.new.handle_message(RosettaQueue::Consumer.receive(consumer.destination))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Consumes the first message on queue and returns it.
|
35
|
+
# Example:
|
36
|
+
# message = consume_once :foo_queue
|
37
|
+
def consume_once(dest)
|
38
|
+
RosettaQueue::Consumer.receive(dest)
|
39
|
+
end
|
40
|
+
|
41
|
+
def consuming_from(destination)
|
42
|
+
sleep 1
|
43
|
+
Messaging::Consumer.receive(destination, :persistent => false).to_hash_from_json
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module RosettaQueue
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
class PublishAMessageTo
|
5
|
+
|
6
|
+
def initialize(expected_queue_name, options=nil)
|
7
|
+
@options = options || {}
|
8
|
+
@how_many_messages_expected = (@options[:exactly] || 1).to_i
|
9
|
+
@expected_queue_name = expected_queue_name
|
10
|
+
@expected_queue = expected_queue_name.is_a?(Symbol) ? RosettaQueue::Destinations.lookup(expected_queue_name) : expected_queue_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(lambda_to_run)
|
14
|
+
#given
|
15
|
+
RosettaQueue::Adapter.stub!(:instance).and_return(fake_adapter = RosettaQueue::Gateway::FakeAdapter.new)
|
16
|
+
#when
|
17
|
+
lambda_to_run.call
|
18
|
+
#then
|
19
|
+
@actual_queues = fake_adapter.queues
|
20
|
+
@number_of_messages_published = @actual_queues.select{ |q| q == @expected_queue}.size
|
21
|
+
@number_of_messages_published == @how_many_messages_expected
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message
|
25
|
+
"expected #{message_plural} published to the #{@expected_queue.inspect} queue but #{@number_of_messages_published} messages were"
|
26
|
+
end
|
27
|
+
|
28
|
+
def negative_failure_message
|
29
|
+
"expected ##{message_plural} NOT to be published to the #{@expected_queue.inspect} queue but that queue was published to #{@number_of_messages_published} times"
|
30
|
+
end
|
31
|
+
|
32
|
+
def description
|
33
|
+
"publish #{message_plural} to the '#{@expected_queue_name}' queue"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def message_plural
|
38
|
+
@how_many_messages_expected == 1 ? "a message" : "#{@how_many_messages_expected} messages"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def publish_a_message_to(expected_queue)
|
43
|
+
PublishAMessageTo.new(expected_queue)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias :publish_message_to :publish_a_message_to
|
47
|
+
|
48
|
+
def publish_messages_to(expected_queue, options)
|
49
|
+
PublishAMessageTo.new(expected_queue, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
class PublishMessageMatcher
|
53
|
+
|
54
|
+
|
55
|
+
def matches?(lambda_to_run)
|
56
|
+
#given
|
57
|
+
RosettaQueue::Adapter.stub!(:instance).and_return(fake_adapter = RosettaQueue::Gateway::FakeAdapter.new)
|
58
|
+
#when
|
59
|
+
lambda_to_run.call
|
60
|
+
#then
|
61
|
+
message = fake_adapter.messages_sent_to(@expected_queue).first || ''
|
62
|
+
@actual_message = message
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
def extract_options(options)
|
67
|
+
if (expected_queue_name = options[:to])
|
68
|
+
@expected_queue = expected_queue_name.is_a?(Symbol) ? RosettaQueue::Destinations.lookup(expected_queue_name) : expected_queue_name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class PublishMessageWith < PublishMessageMatcher
|
74
|
+
|
75
|
+
def initialize(message_subset, options)
|
76
|
+
@message_subset = message_subset
|
77
|
+
extract_options(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def matches?(lambda_to_run)
|
81
|
+
super
|
82
|
+
Spec::Mocks::ArgumentConstraints::HashIncludingConstraint.new(@message_subset) == @actual_message
|
83
|
+
end
|
84
|
+
|
85
|
+
def failure_message
|
86
|
+
if @actual_message.blank?
|
87
|
+
"expected #{@message_subset.inspect} to be contained in a message but no message was published"
|
88
|
+
else
|
89
|
+
"expected #{@message_subset.inspect} to be contained in the message: #{@actual_message.inspect}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def negative_failure_message
|
94
|
+
"expected #{@message_subset.inspect} not to be contained in the message but was"
|
95
|
+
end
|
96
|
+
|
97
|
+
def description
|
98
|
+
"publish a message with #{@message_subset.inspect}"
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
def publish_message_with(message_subset, options={})
|
104
|
+
PublishMessageWith.new(message_subset, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
class PublishMessage < PublishMessageMatcher
|
109
|
+
|
110
|
+
def initialize(expected_message, options)
|
111
|
+
@expected_message = expected_message
|
112
|
+
extract_options(options)
|
113
|
+
end
|
114
|
+
|
115
|
+
def matches?(lambda_to_run)
|
116
|
+
super
|
117
|
+
@actual_message == @expected_message
|
118
|
+
end
|
119
|
+
|
120
|
+
def failure_message
|
121
|
+
if @actual_message.blank?
|
122
|
+
"expected #{@expected_message.inspect} to be published but no message was"
|
123
|
+
else
|
124
|
+
"expected #{@expected_message.inspect} to be published but the following was instead: #{@actual_message.inspect}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def negative_failure_message
|
129
|
+
"expected #{@expected_message.inspect} not to be published but it was"
|
130
|
+
end
|
131
|
+
|
132
|
+
def description
|
133
|
+
"publish the message: #{@expected_message.inspect}"
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
def publish_message(exact_expected_message, options={})
|
139
|
+
PublishMessage.new(exact_expected_message, options)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|