freddy 0.3.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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.npmignore +8 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +49 -0
- data/LICENCE.txt +22 -0
- data/README.md +163 -0
- data/Rakefile +7 -0
- data/freddy.gemspec +25 -0
- data/lib/freddy.rb +86 -0
- data/lib/freddy/consumer.rb +64 -0
- data/lib/freddy/delivery.rb +10 -0
- data/lib/freddy/message_handler.rb +20 -0
- data/lib/freddy/message_handlers.rb +67 -0
- data/lib/freddy/producer.rb +28 -0
- data/lib/freddy/request.rb +106 -0
- data/lib/freddy/request_manager.rb +41 -0
- data/lib/freddy/responder_handler.rb +30 -0
- data/lib/freddy/sync_response_container.rb +32 -0
- data/spec/freddy/consumer_spec.rb +23 -0
- data/spec/freddy/freddy_spec.rb +223 -0
- data/spec/freddy/message_handler_spec.rb +27 -0
- data/spec/freddy/request_spec.rb +38 -0
- data/spec/freddy/responder_handler_spec.rb +33 -0
- data/spec/integration/logging_spec.rb +33 -0
- data/spec/spec_helper.rb +45 -0
- metadata +150 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
class Freddy
|
2
|
+
class MessageHandler
|
3
|
+
attr_reader :destination, :correlation_id
|
4
|
+
|
5
|
+
def initialize(adapter, delivery)
|
6
|
+
@adapter = adapter
|
7
|
+
@properties = delivery.properties
|
8
|
+
@destination = @properties[:destination]
|
9
|
+
@correlation_id = @properties[:correlation_id]
|
10
|
+
end
|
11
|
+
|
12
|
+
def success(response = nil)
|
13
|
+
@adapter.success(@properties[:reply_to], response)
|
14
|
+
end
|
15
|
+
|
16
|
+
def error(error = {error: "Couldn't process message"})
|
17
|
+
@adapter.error(@properties[:reply_to], error)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class Freddy
|
2
|
+
module MessageHandlers
|
3
|
+
def self.for_type(type)
|
4
|
+
type == 'request' ? RequestHandler : StandardMessageHandler
|
5
|
+
end
|
6
|
+
|
7
|
+
class StandardMessageHandler
|
8
|
+
def initialize(producer, logger)
|
9
|
+
@producer = producer
|
10
|
+
@logger = logger
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle_message(payload, msg_handler, &block)
|
14
|
+
block.call payload, msg_handler
|
15
|
+
rescue Exception => e
|
16
|
+
destination = msg_handler.destination
|
17
|
+
@logger.error "Exception occured while processing message from #{destination}: #{Freddy.format_exception(e)}"
|
18
|
+
Freddy.notify_exception(e, destination: destination)
|
19
|
+
end
|
20
|
+
|
21
|
+
def success(*)
|
22
|
+
# NOP
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(*)
|
26
|
+
# NOP
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class RequestHandler
|
31
|
+
def initialize(producer, logger)
|
32
|
+
@producer = producer
|
33
|
+
@logger = logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle_message(payload, msg_handler, &block)
|
37
|
+
@correlation_id = msg_handler.correlation_id
|
38
|
+
|
39
|
+
if !@correlation_id
|
40
|
+
@logger.error "Received request without correlation_id"
|
41
|
+
Freddy.notify_exception(e)
|
42
|
+
else
|
43
|
+
block.call payload, msg_handler
|
44
|
+
end
|
45
|
+
rescue Exception => e
|
46
|
+
@logger.error "Exception occured while handling the request with correlation_id #{@correlation_id}: #{Freddy.format_exception(e)}"
|
47
|
+
Freddy.notify_exception(e, destination: msg_handler.destination, correlation_id: @correlation_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
def success(reply_to, response)
|
51
|
+
send_response(reply_to, response, type: 'success')
|
52
|
+
end
|
53
|
+
|
54
|
+
def error(reply_to, response)
|
55
|
+
send_response(reply_to, response, type: 'error')
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def send_response(reply_to, response, opts = {})
|
61
|
+
@producer.produce reply_to.force_encoding('utf-8'), response, {
|
62
|
+
correlation_id: @correlation_id
|
63
|
+
}.merge(opts)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'request'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class Freddy
|
5
|
+
class Producer
|
6
|
+
CONTENT_TYPE = 'application/json'.freeze
|
7
|
+
|
8
|
+
def initialize(channel, logger)
|
9
|
+
@channel, @logger = channel, logger
|
10
|
+
@exchange = @channel.default_exchange
|
11
|
+
@topic_exchange = @channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
|
12
|
+
end
|
13
|
+
|
14
|
+
def produce(destination, payload, properties={})
|
15
|
+
@logger.debug "Producing message #{payload.inspect} to #{destination}"
|
16
|
+
|
17
|
+
properties = properties.merge(routing_key: destination, content_type: CONTENT_TYPE)
|
18
|
+
json_payload = payload.to_json
|
19
|
+
|
20
|
+
@topic_exchange.publish json_payload, properties.dup
|
21
|
+
@exchange.publish json_payload, properties.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_return(*args, &block)
|
25
|
+
@exchange.on_return(*args, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require_relative 'producer'
|
2
|
+
require_relative 'consumer'
|
3
|
+
require_relative 'request_manager'
|
4
|
+
require_relative 'sync_response_container'
|
5
|
+
require_relative 'message_handlers'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'hamster/mutable_hash'
|
8
|
+
|
9
|
+
class Freddy
|
10
|
+
class Request
|
11
|
+
NO_ROUTE = 312
|
12
|
+
|
13
|
+
class EmptyRequest < Exception
|
14
|
+
end
|
15
|
+
|
16
|
+
class EmptyResponder < Exception
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(channel, logger)
|
20
|
+
@channel, @logger = channel, logger
|
21
|
+
@producer, @consumer = Producer.new(channel, logger), Consumer.new(channel, logger)
|
22
|
+
@listening_for_responses = false
|
23
|
+
@request_map = Hamster.mutable_hash
|
24
|
+
@request_manager = RequestManager.new @request_map, @logger
|
25
|
+
|
26
|
+
@producer.on_return do |return_info, properties, content|
|
27
|
+
if return_info[:reply_code] == NO_ROUTE
|
28
|
+
@request_manager.no_route(properties[:correlation_id])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def sync_request(destination, payload, opts)
|
34
|
+
timeout_seconds = opts.fetch(:timeout)
|
35
|
+
container = SyncResponseContainer.new
|
36
|
+
async_request destination, payload, opts, &container
|
37
|
+
container.wait_for_response(timeout_seconds + 0.1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def async_request(destination, payload, timeout:, delete_on_timeout:, **options, &block)
|
41
|
+
listen_for_responses unless @listening_for_responses
|
42
|
+
|
43
|
+
correlation_id = SecureRandom.uuid
|
44
|
+
@request_map.store(correlation_id, callback: block, destination: destination, timeout: Time.now + timeout)
|
45
|
+
|
46
|
+
@logger.debug "Publishing request to #{destination}, waiting for response on #{@response_queue.name} with correlation_id #{correlation_id}"
|
47
|
+
|
48
|
+
if delete_on_timeout
|
49
|
+
options[:expiration] = (timeout * 1000).to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
@producer.produce destination, payload, options.merge(
|
53
|
+
correlation_id: correlation_id, reply_to: @response_queue.name,
|
54
|
+
mandatory: true, type: 'request'
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_to(destination, &block)
|
59
|
+
raise EmptyResponder unless block
|
60
|
+
@response_queue = create_response_queue unless @response_queue
|
61
|
+
@logger.debug "Listening for requests on #{destination}"
|
62
|
+
|
63
|
+
responder_handler = @consumer.consume destination do |payload, delivery|
|
64
|
+
handler = MessageHandlers.for_type(delivery.properties[:type]).new(@producer, @logger)
|
65
|
+
|
66
|
+
msg_handler = MessageHandler.new(handler, delivery)
|
67
|
+
handler.handle_message payload, msg_handler, &block
|
68
|
+
end
|
69
|
+
responder_handler
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def create_response_queue
|
75
|
+
@channel.queue("", exclusive: true)
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_response(payload, delivery)
|
79
|
+
correlation_id = delivery.properties[:correlation_id]
|
80
|
+
request = @request_map[correlation_id]
|
81
|
+
if request
|
82
|
+
@logger.debug "Got response for request to #{request[:destination]} with correlation_id #{correlation_id}"
|
83
|
+
@request_map.delete correlation_id
|
84
|
+
request[:callback].call payload, delivery
|
85
|
+
else
|
86
|
+
message = "Got rpc response for correlation_id #{correlation_id} but there is no requester"
|
87
|
+
@logger.warn message
|
88
|
+
Freddy.notify 'NoRequesterForResponse', message, correlation_id: correlation_id
|
89
|
+
end
|
90
|
+
rescue Exception => e
|
91
|
+
destination_report = request ? "to #{request[:destination]}" : ''
|
92
|
+
@logger.error "Exception occured while handling the response of request made #{destination_report} with correlation_id #{correlation_id}: #{Freddy.format_exception e}"
|
93
|
+
Freddy.notify_exception(e, destination: request[:destination], correlation_id: correlation_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
def listen_for_responses
|
97
|
+
@listening_for_responses = true
|
98
|
+
@response_queue = create_response_queue unless @response_queue
|
99
|
+
@request_manager.start
|
100
|
+
@consumer.consume_from_queue @response_queue do |payload, delivery|
|
101
|
+
handle_response payload, delivery
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Freddy
|
2
|
+
class RequestManager
|
3
|
+
|
4
|
+
def initialize(requests, logger)
|
5
|
+
@requests, @logger = requests, logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def start
|
9
|
+
@timeout_thread = Thread.new do
|
10
|
+
while true do
|
11
|
+
clear_timeouts Time.now
|
12
|
+
sleep 0.05
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def no_route(correlation_id)
|
18
|
+
if request = @requests[correlation_id]
|
19
|
+
@requests.delete correlation_id
|
20
|
+
request[:callback].call({error: 'Specified queue does not exist'}, nil)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def clear_timeouts(now)
|
27
|
+
@requests.each do |key, value|
|
28
|
+
timeout(key, value) if now > value[:timeout]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def timeout(correlation_id, request)
|
33
|
+
@requests.delete correlation_id
|
34
|
+
|
35
|
+
message = "Request #{correlation_id} timed out waiting response from #{request[:destination]} with timeout #{request[:timeout]}"
|
36
|
+
@logger.warn message
|
37
|
+
Freddy.notify 'RequestTimeout', message, request: correlation_id, destination: request[:destination], timeout: request[:timeout]
|
38
|
+
request[:callback].call({error: 'Timed out waiting for response'}, nil)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Freddy
|
2
|
+
class ResponderHandler
|
3
|
+
|
4
|
+
def initialize(consumer, channel)
|
5
|
+
@consumer = consumer
|
6
|
+
@channel = channel
|
7
|
+
end
|
8
|
+
|
9
|
+
def cancel
|
10
|
+
@consumer.cancel
|
11
|
+
end
|
12
|
+
|
13
|
+
def queue
|
14
|
+
@consumer.queue
|
15
|
+
end
|
16
|
+
|
17
|
+
def destroy_destination
|
18
|
+
@consumer.queue.delete
|
19
|
+
end
|
20
|
+
|
21
|
+
def join
|
22
|
+
@channel.work_pool.join
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutdown
|
26
|
+
@channel.work_pool.shutdown
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
class Freddy
|
4
|
+
class SyncResponseContainer
|
5
|
+
def call(response, delivery)
|
6
|
+
@response = response
|
7
|
+
@delivery = delivery
|
8
|
+
end
|
9
|
+
|
10
|
+
def wait_for_response(timeout)
|
11
|
+
Timeout::timeout(timeout) do
|
12
|
+
sleep 0.001 until filled?
|
13
|
+
end
|
14
|
+
|
15
|
+
if !@delivery || @delivery.properties[:type] == 'error'
|
16
|
+
raise ErrorResponse.new(@response)
|
17
|
+
else
|
18
|
+
@response
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def to_proc
|
25
|
+
Proc.new {|*args| self.call(*args)}
|
26
|
+
end
|
27
|
+
|
28
|
+
def filled?
|
29
|
+
!@response.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Freddy::Consumer do
|
4
|
+
let(:freddy) { Freddy.build(logger, config) }
|
5
|
+
|
6
|
+
let(:destination) { random_destination }
|
7
|
+
let(:payload) { {pay: 'load'} }
|
8
|
+
|
9
|
+
let(:consumer) { freddy.consumer }
|
10
|
+
|
11
|
+
it 'raises exception when no consumer is provided' do
|
12
|
+
expect { consumer.consume destination }.to raise_error described_class::EmptyConsumer
|
13
|
+
end
|
14
|
+
|
15
|
+
it "doesn't call passed block without any messages" do
|
16
|
+
consumer.consume destination do
|
17
|
+
@message_received = true
|
18
|
+
end
|
19
|
+
default_sleep
|
20
|
+
|
21
|
+
expect(@message_received).to be_falsy
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Freddy do
|
4
|
+
let(:freddy) { described_class.build(logger, config) }
|
5
|
+
|
6
|
+
let(:destination) { random_destination }
|
7
|
+
let(:destination2) { random_destination }
|
8
|
+
let(:payload) { {pay: 'load'} }
|
9
|
+
|
10
|
+
def respond_to(&block)
|
11
|
+
freddy.respond_to(destination, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when making a send-and-forget request' do
|
15
|
+
context 'with timeout' do
|
16
|
+
it 'removes the message from the queue after the timeout' do
|
17
|
+
# Assume that there already is a queue. Otherwise will get an early
|
18
|
+
# return.
|
19
|
+
freddy.channel.queue(destination)
|
20
|
+
|
21
|
+
freddy.deliver(destination, {}, timeout: 0.1)
|
22
|
+
sleep 0.2
|
23
|
+
|
24
|
+
processed_after_timeout = false
|
25
|
+
respond_to { processed_after_timeout = true }
|
26
|
+
default_sleep
|
27
|
+
|
28
|
+
expect(processed_after_timeout).to be(false)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'without timeout' do
|
33
|
+
it 'keeps the message in the queue' do
|
34
|
+
# Assume that there already is a queue. Otherwise will get an early
|
35
|
+
# return.
|
36
|
+
freddy.channel.queue(destination)
|
37
|
+
|
38
|
+
freddy.deliver(destination, {})
|
39
|
+
default_sleep # to ensure everything is properly cleaned
|
40
|
+
|
41
|
+
processed_after_timeout = false
|
42
|
+
respond_to { processed_after_timeout = true }
|
43
|
+
default_sleep
|
44
|
+
|
45
|
+
expect(processed_after_timeout).to be(true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when making a synchronized request' do
|
51
|
+
it 'returns response as soon as possible' do
|
52
|
+
respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
|
53
|
+
response = freddy.deliver_with_response(destination, {a: 'b'})
|
54
|
+
|
55
|
+
expect(response).to eq(res: 'yey')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'raises an error if the message was errored' do
|
59
|
+
respond_to { |payload, msg_handler| msg_handler.error(error: 'not today') }
|
60
|
+
|
61
|
+
expect {
|
62
|
+
freddy.deliver_with_response(destination, payload)
|
63
|
+
}.to raise_error(Freddy::ErrorResponse) {|error|
|
64
|
+
expect(error.response).to eq(error: 'not today')
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'does not leak consumers' do
|
69
|
+
respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
|
70
|
+
|
71
|
+
old_count = freddy.channel.consumers.keys.count
|
72
|
+
|
73
|
+
response1 = freddy.deliver_with_response(destination, {a: 'b'})
|
74
|
+
response2 = freddy.deliver_with_response(destination, {a: 'b'})
|
75
|
+
|
76
|
+
expect(response1).to eq(res: 'yey')
|
77
|
+
expect(response2).to eq(res: 'yey')
|
78
|
+
|
79
|
+
new_count = freddy.channel.consumers.keys.count
|
80
|
+
expect(new_count).to be(old_count + 1)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'responds to the correct requester' do
|
84
|
+
respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
|
85
|
+
|
86
|
+
response = freddy.deliver_with_response(destination, payload)
|
87
|
+
expect(response).to eq(res: 'yey')
|
88
|
+
|
89
|
+
expect {
|
90
|
+
freddy.deliver_with_response(destination2, payload)
|
91
|
+
}.to raise_error(Freddy::ErrorResponse)
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when queue does not exist' do
|
95
|
+
it 'gives a no route error' do
|
96
|
+
begin
|
97
|
+
Timeout::timeout(0.5) do
|
98
|
+
expect {
|
99
|
+
freddy.deliver_with_response(destination, {a: 'b'}, timeout: 3)
|
100
|
+
}.to raise_error(Freddy::ErrorResponse) {|error|
|
101
|
+
expect(error.response).to eq(error: 'Specified queue does not exist')
|
102
|
+
}
|
103
|
+
end
|
104
|
+
rescue Timeout::Error
|
105
|
+
fail('Received a timeout error instead of the no route error')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'on timeout' do
|
111
|
+
it 'gives timeout error' do
|
112
|
+
respond_to { |payload, msg_handler| sleep 0.2 }
|
113
|
+
|
114
|
+
expect {
|
115
|
+
freddy.deliver_with_response(destination, {a: 'b'}, timeout: 0.1)
|
116
|
+
}.to raise_error(Freddy::ErrorResponse) {|error|
|
117
|
+
expect(error.response).to eq(error: 'Timed out waiting for response')
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'with delete_on_timeout is set to true' do
|
122
|
+
it 'removes the message from the queue' do
|
123
|
+
# Assume that there already is a queue. Otherwise will get an early
|
124
|
+
# return.
|
125
|
+
freddy.channel.queue(destination)
|
126
|
+
|
127
|
+
expect {
|
128
|
+
freddy.deliver_with_response(destination, {}, timeout: 0.1)
|
129
|
+
}.to raise_error(Freddy::ErrorResponse)
|
130
|
+
default_sleep # to ensure everything is properly cleaned
|
131
|
+
|
132
|
+
processed_after_timeout = false
|
133
|
+
respond_to { processed_after_timeout = true }
|
134
|
+
default_sleep
|
135
|
+
|
136
|
+
expect(processed_after_timeout).to be(false)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'with delete_on_timeout is set to false' do
|
141
|
+
it 'removes the message from the queue' do
|
142
|
+
# Assume that there already is a queue. Otherwise will get an early
|
143
|
+
# return.
|
144
|
+
freddy.channel.queue(destination)
|
145
|
+
|
146
|
+
expect {
|
147
|
+
freddy.deliver_with_response(destination, {}, timeout: 0.1, delete_on_timeout: false)
|
148
|
+
}.to raise_error(Freddy::ErrorResponse)
|
149
|
+
default_sleep # to ensure everything is properly cleaned
|
150
|
+
|
151
|
+
processed_after_timeout = false
|
152
|
+
respond_to { processed_after_timeout = true }
|
153
|
+
default_sleep
|
154
|
+
|
155
|
+
expect(processed_after_timeout).to be(true)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'when tapping' do
|
162
|
+
def tap(custom_destination = destination, &block)
|
163
|
+
freddy.tap_into(custom_destination, &block)
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'receives messages' do
|
167
|
+
tap {|msg| @tapped_message = msg }
|
168
|
+
deliver
|
169
|
+
|
170
|
+
wait_for { @tapped_message }
|
171
|
+
expect(@tapped_message).to eq(payload)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'has the destination' do
|
175
|
+
tap "somebody.*.love" do |message, destination|
|
176
|
+
@destination = destination
|
177
|
+
end
|
178
|
+
deliver "somebody.to.love"
|
179
|
+
|
180
|
+
wait_for { @destination }
|
181
|
+
expect(@destination).to eq("somebody.to.love")
|
182
|
+
end
|
183
|
+
|
184
|
+
it "doesn't consume the message" do
|
185
|
+
tap { @tapped = true }
|
186
|
+
respond_to { @message_received = true }
|
187
|
+
|
188
|
+
deliver
|
189
|
+
|
190
|
+
wait_for { @tapped }
|
191
|
+
wait_for { @message_received }
|
192
|
+
expect(@tapped).to be(true)
|
193
|
+
expect(@message_received).to be(true)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "allows * wildcard" do
|
197
|
+
tap("somebody.*.love") { @tapped = true }
|
198
|
+
|
199
|
+
deliver "somebody.to.love"
|
200
|
+
|
201
|
+
wait_for { @tapped }
|
202
|
+
expect(@tapped).to be(true)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "* matches only one word" do
|
206
|
+
tap("somebody.*.love") { @tapped = true }
|
207
|
+
|
208
|
+
deliver "somebody.not.to.love"
|
209
|
+
|
210
|
+
default_sleep
|
211
|
+
expect(@tapped).to be_falsy
|
212
|
+
end
|
213
|
+
|
214
|
+
it "allows # wildcard" do
|
215
|
+
tap("i.#.free") { @tapped = true }
|
216
|
+
|
217
|
+
deliver "i.want.to.break.free"
|
218
|
+
|
219
|
+
wait_for { @tapped }
|
220
|
+
expect(@tapped).to be(true)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|