stomper 0.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/README.rdoc +63 -11
- data/lib/stomper.rb +13 -1
- data/lib/stomper/client.rb +3 -284
- data/lib/stomper/connection.rb +67 -114
- data/lib/stomper/frame_reader.rb +73 -0
- data/lib/stomper/frame_writer.rb +21 -0
- data/lib/stomper/frames.rb +24 -9
- data/lib/stomper/frames/abort.rb +2 -6
- data/lib/stomper/frames/ack.rb +2 -6
- data/lib/stomper/frames/begin.rb +2 -5
- data/lib/stomper/frames/client_frame.rb +30 -27
- data/lib/stomper/frames/commit.rb +1 -5
- data/lib/stomper/frames/connect.rb +2 -7
- data/lib/stomper/frames/connected.rb +11 -8
- data/lib/stomper/frames/disconnect.rb +1 -4
- data/lib/stomper/frames/error.rb +3 -8
- data/lib/stomper/frames/message.rb +15 -11
- data/lib/stomper/frames/receipt.rb +1 -6
- data/lib/stomper/frames/send.rb +1 -5
- data/lib/stomper/frames/server_frame.rb +13 -23
- data/lib/stomper/frames/subscribe.rb +9 -14
- data/lib/stomper/frames/unsubscribe.rb +3 -7
- data/lib/stomper/open_uri_interface.rb +41 -0
- data/lib/stomper/receipt_handlers.rb +23 -0
- data/lib/stomper/receiptor.rb +38 -0
- data/lib/stomper/sockets.rb +37 -0
- data/lib/stomper/subscriber.rb +76 -0
- data/lib/stomper/subscription.rb +14 -14
- data/lib/stomper/threaded_receiver.rb +59 -0
- data/lib/stomper/transaction.rb +13 -8
- data/lib/stomper/transactor.rb +50 -0
- data/lib/stomper/uri.rb +55 -0
- data/spec/client_spec.rb +7 -158
- data/spec/connection_spec.rb +13 -3
- data/spec/frame_reader_spec.rb +37 -0
- data/spec/frame_writer_spec.rb +27 -0
- data/spec/frames/client_frame_spec.rb +22 -98
- data/spec/frames/indirect_frame_spec.rb +45 -0
- data/spec/frames/server_frame_spec.rb +15 -16
- data/spec/open_uri_interface_spec.rb +132 -0
- data/spec/receiptor_spec.rb +35 -0
- data/spec/shared_connection_examples.rb +12 -17
- data/spec/spec_helper.rb +6 -0
- data/spec/subscriber_spec.rb +77 -0
- data/spec/subscription_spec.rb +11 -11
- data/spec/subscriptions_spec.rb +3 -6
- data/spec/threaded_receiver_spec.rb +33 -0
- data/spec/transaction_spec.rb +5 -5
- data/spec/transactor_spec.rb +46 -0
- metadata +30 -6
- data/lib/stomper/frames/headers.rb +0 -68
- data/spec/frames/headers_spec.rb +0 -54
@@ -0,0 +1,73 @@
|
|
1
|
+
module Stomper
|
2
|
+
# Deserializes Stomp Frames from an input stream.
|
3
|
+
# Any object that responds appropriately to +getc+, +gets+
|
4
|
+
# and +read+ can be used as the input stream.
|
5
|
+
module FrameReader
|
6
|
+
# Receives the next Stomp Frame from the socket stream
|
7
|
+
def receive_frame
|
8
|
+
command = read_command
|
9
|
+
headers = read_headers
|
10
|
+
body = read_body(headers[:'content-length'])
|
11
|
+
Stomper::Frames::ServerFrame.build(command, headers, body)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def read_command
|
16
|
+
command = ''
|
17
|
+
while(command.size == 0)
|
18
|
+
command = gets(Stomper::Frames::LINE_DELIMITER).chomp!
|
19
|
+
end
|
20
|
+
command
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_headers
|
24
|
+
headers = {}
|
25
|
+
loop do
|
26
|
+
line = gets(Stomper::Frames::LINE_DELIMITER).chomp!
|
27
|
+
break if line.size == 0
|
28
|
+
if (delim = line.index(':'))
|
29
|
+
headers[ line[0..(delim-1)].to_sym ] = line[(delim+1)..-1]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
headers
|
33
|
+
end
|
34
|
+
|
35
|
+
def read_body(body_len)
|
36
|
+
body_len &&= body_len.strip.to_i
|
37
|
+
if body_len
|
38
|
+
read_fixed_body(body_len)
|
39
|
+
else
|
40
|
+
read_null_terminated_body
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_null_terminated_body
|
45
|
+
body = ''
|
46
|
+
while next_byte = get_body_byte
|
47
|
+
body << next_byte.chr
|
48
|
+
end
|
49
|
+
body
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_fixed_body(num_bytes)
|
53
|
+
body = read(num_bytes)
|
54
|
+
raise MalformedFrameError if get_body_byte
|
55
|
+
body
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_body_byte
|
59
|
+
next_byte = get_ord
|
60
|
+
(next_byte == Stomper::Frames::TERMINATOR) ? nil : next_byte
|
61
|
+
end
|
62
|
+
|
63
|
+
if String.method_defined?(:ord)
|
64
|
+
def get_ord
|
65
|
+
getc.ord
|
66
|
+
end
|
67
|
+
else
|
68
|
+
def get_ord
|
69
|
+
getc
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Stomper
|
2
|
+
# Serializes Stomp Frames to an output stream.
|
3
|
+
# Any object that responds appropriately to +write+
|
4
|
+
# can be used as the input stream.
|
5
|
+
module FrameWriter
|
6
|
+
# Writes a Stomp Frame to the underlying output stream.
|
7
|
+
def transmit_frame(frame)
|
8
|
+
write([ frame.command, Stomper::Frames::LINE_DELIMITER,
|
9
|
+
serialize_headers(frame.headers), Stomper::Frames::LINE_DELIMITER,
|
10
|
+
frame.body, Stomper::Frames::TERMINATOR.chr].join)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def serialize_headers(headers)
|
15
|
+
headers.inject("") do |acc, (key, val)|
|
16
|
+
acc << "#{key}#{Stomper::Frames::HEADER_DELIMITER}#{val}#{Stomper::Frames::LINE_DELIMITER}"
|
17
|
+
acc
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/stomper/frames.rb
CHANGED
@@ -1,4 +1,27 @@
|
|
1
|
-
|
1
|
+
module Stomper
|
2
|
+
# This module holds all known encapsulations of
|
3
|
+
# frames that are part of the Stomp Protocol specification.
|
4
|
+
module Frames
|
5
|
+
HEADER_DELIMITER = ':'
|
6
|
+
TERMINATOR = 0
|
7
|
+
LINE_DELIMITER = "\n"
|
8
|
+
|
9
|
+
class IndirectFrame #:nodoc:
|
10
|
+
attr_reader :headers, :body
|
11
|
+
|
12
|
+
def initialize(headers={}, body=nil, command=nil)
|
13
|
+
@command = command && command.to_s.upcase
|
14
|
+
@headers = headers.dup
|
15
|
+
@body = body
|
16
|
+
end
|
17
|
+
|
18
|
+
def command
|
19
|
+
@command ||= self.class.name.split("::").last.upcase
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
2
25
|
require 'stomper/frames/client_frame'
|
3
26
|
require 'stomper/frames/server_frame'
|
4
27
|
require 'stomper/frames/abort'
|
@@ -14,11 +37,3 @@ require 'stomper/frames/receipt'
|
|
14
37
|
require 'stomper/frames/send'
|
15
38
|
require 'stomper/frames/subscribe'
|
16
39
|
require 'stomper/frames/unsubscribe'
|
17
|
-
|
18
|
-
module Stomper
|
19
|
-
# This module holds all known encapsulations of
|
20
|
-
# frames that are part of the
|
21
|
-
# {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
22
|
-
module Frames
|
23
|
-
end
|
24
|
-
end
|
data/lib/stomper/frames/abort.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
|
-
# Encapsulates an "
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
3
|
+
# Encapsulates an "ABORT" frame from the Stomp Protocol.
|
7
4
|
class Abort < Stomper::Frames::ClientFrame
|
8
5
|
def initialize(transaction_id, headers={})
|
9
|
-
super(
|
10
|
-
@headers.transaction = transaction_id
|
6
|
+
super(headers.merge(:transaction => transaction_id))
|
11
7
|
end
|
12
8
|
end
|
13
9
|
end
|
data/lib/stomper/frames/ack.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates an "ACK" frame from the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Ack < Stomper::Frames::ClientFrame
|
8
5
|
def initialize(message_id, headers={})
|
9
|
-
super('
|
10
|
-
@headers["message-id"] = message_id
|
6
|
+
super(headers.merge({ :'message-id' => message_id }))
|
11
7
|
end
|
12
8
|
|
13
9
|
# Creates a new Ack instance that corresponds to an acknowledgement
|
@@ -18,7 +14,7 @@ module Stomper
|
|
18
14
|
# +message+ will be injected into the newly created Ack object's headers.
|
19
15
|
def self.ack_for(message, headers = {})
|
20
16
|
if message.is_a?(Message)
|
21
|
-
headers[
|
17
|
+
headers[:transaction] = message.headers[:transaction] if message.headers[:transaction]
|
22
18
|
new(message.id, headers)
|
23
19
|
else
|
24
20
|
new(message.to_s)
|
data/lib/stomper/frames/begin.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "BEGIN" frame from the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Begin < Stomper::Frames::ClientFrame
|
8
5
|
def initialize(transaction_id, headers={})
|
9
|
-
super(
|
10
|
-
@headers
|
6
|
+
super(headers.merge(:transaction => transaction_id))
|
7
|
+
@headers[:transaction] = transaction_id
|
11
8
|
end
|
12
9
|
end
|
13
10
|
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a client side frame for the Stomp Protocol.
|
4
|
-
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
|
-
class ClientFrame
|
8
|
-
attr_reader :headers, :body, :command
|
4
|
+
class ClientFrame < IndirectFrame
|
9
5
|
|
10
6
|
# Creates a new ClientFrame instance with the specified +command+,
|
11
7
|
# +headers+ and +body+.
|
@@ -14,14 +10,12 @@ module Stomper
|
|
14
10
|
# generated for this particular frame instance. This key can be
|
15
11
|
# specified in the +headers+ parameter of any of the subclasses of ClientFrame,
|
16
12
|
# and it will be interpretted in the same fashion.
|
17
|
-
def initialize(
|
18
|
-
@command = command
|
13
|
+
def initialize(headers={}, body=nil, command = nil)
|
19
14
|
@generate_content_length = headers.delete(:generate_content_length)
|
20
|
-
|
21
|
-
@body = body
|
15
|
+
super(headers, body, command)
|
22
16
|
end
|
23
17
|
|
24
|
-
# If +bool+ false or nil, this frame instance will not attempt to
|
18
|
+
# If +bool+ is false or nil, this frame instance will not attempt to
|
25
19
|
# automatically generate a content-length header. This is useful when
|
26
20
|
# dealing with ActiveMQ as a stomp message broker, which will treat incoming
|
27
21
|
# messages lacking a content-length header as +TextMessage+s and
|
@@ -38,15 +32,15 @@ module Stomper
|
|
38
32
|
@generate_content_length.nil? ? self.class.generate_content_length? : @generate_content_length
|
39
33
|
end
|
40
34
|
|
41
|
-
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
35
|
+
|
36
|
+
# Returns the headers for this frame, including a +content-length+ header
|
37
|
+
# set to the size of the current body, if +generate_content_length?+ is
|
38
|
+
# not false.
|
39
|
+
def headers
|
40
|
+
if generate_content_length? && @body && !@body.empty?
|
41
|
+
@headers[:'content-length'] ||= body_size
|
42
|
+
end
|
43
|
+
@headers
|
50
44
|
end
|
51
45
|
|
52
46
|
class << self
|
@@ -60,7 +54,8 @@ module Stomper
|
|
60
54
|
# method, or true if no value has been set (thus, defaults to true.)
|
61
55
|
#
|
62
56
|
# The precedence for resolving whether or not a content-length header
|
63
|
-
# is generated by the
|
57
|
+
# is generated by the +headers+ method is:
|
58
|
+
# check the instance setting,
|
64
59
|
# if it has not been set, defer to the class setting, if it hasn't
|
65
60
|
# been set, default to true.
|
66
61
|
def generate_content_length?
|
@@ -69,16 +64,24 @@ module Stomper
|
|
69
64
|
end
|
70
65
|
@generate_content_length
|
71
66
|
end
|
67
|
+
|
68
|
+
def inherited(client_frame) #:nodoc:
|
69
|
+
declared_frames << { :class => client_frame, :command => client_frame.name.split("::").last.downcase.to_sym }
|
70
|
+
end
|
71
|
+
|
72
|
+
def declared_frames
|
73
|
+
@declared_frames ||= []
|
74
|
+
end
|
72
75
|
end
|
73
76
|
|
74
77
|
private
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
if String.method_defined?(:bytesize)
|
79
|
+
def body_size
|
80
|
+
@body.bytesize
|
81
|
+
end
|
82
|
+
else
|
83
|
+
def body_size
|
84
|
+
@body.size
|
82
85
|
end
|
83
86
|
end
|
84
87
|
end
|
@@ -1,13 +1,9 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "COMMIT" frame from the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Commit < Stomper::Frames::ClientFrame
|
8
5
|
def initialize(transaction_id, headers={})
|
9
|
-
super(
|
10
|
-
@headers.transaction = transaction_id
|
6
|
+
super(headers.merge({ :transaction => transaction_id }))
|
11
7
|
end
|
12
8
|
end
|
13
9
|
end
|
@@ -1,14 +1,9 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "CONNECT" frame from the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Connect < Stomper::Frames::ClientFrame
|
8
|
-
def initialize(
|
9
|
-
super(
|
10
|
-
@headers.login = username
|
11
|
-
@headers.passcode = password
|
5
|
+
def initialize(login='', passcode='', headers={})
|
6
|
+
super(headers.merge({ :login => login, :passcode => passcode }))
|
12
7
|
end
|
13
8
|
end
|
14
9
|
end
|
@@ -1,26 +1,29 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "CONNECTED" server side frame for the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Connected < Stomper::Frames::ServerFrame
|
8
|
-
# This class is a factory for incoming 'CONNECTED' commands.
|
9
|
-
factory_for :connected
|
10
5
|
|
11
6
|
# Builds a Connected frame instance with the supplied
|
12
7
|
# +headers+ and +body+
|
13
8
|
def initialize(headers, body)
|
14
|
-
super(
|
9
|
+
super(headers, body)
|
15
10
|
end
|
16
11
|
|
17
12
|
# A convenience method that returns the value of
|
18
13
|
# the session header, if it is set.
|
19
14
|
#
|
20
15
|
# This value can also be accessed as:
|
21
|
-
# frame.headers
|
16
|
+
# frame.headers[:session]
|
22
17
|
def session
|
23
|
-
@headers
|
18
|
+
@headers[:session]
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform
|
22
|
+
# TODO: I want the frames, particularly the server frames, to know
|
23
|
+
# 'what to do' when they are received. For instance, when a CONNECTED
|
24
|
+
# frame is received, the connection it is received on should be marked
|
25
|
+
# as being "connected". This way we can get rid of the various conditional
|
26
|
+
# behavior based on Frame classes in connection and client.
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
@@ -1,12 +1,9 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "DISCONNECT" frame from the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Disconnect < Stomper::Frames::ClientFrame
|
8
5
|
def initialize(headers={})
|
9
|
-
super(
|
6
|
+
super(headers)
|
10
7
|
end
|
11
8
|
end
|
12
9
|
end
|
data/lib/stomper/frames/error.rb
CHANGED
@@ -1,25 +1,20 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates an "ERROR" server side frame for the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Error < Stomper::Frames::ServerFrame
|
8
|
-
# This class is a factory for all incoming ERROR frames.
|
9
|
-
factory_for :error
|
10
5
|
|
11
6
|
# Creates a new Error frame with the supplied +headers+ and +body+
|
12
7
|
def initialize(headers, body)
|
13
|
-
super(
|
8
|
+
super(headers, body)
|
14
9
|
end
|
15
10
|
|
16
11
|
# Returns the message responsible for the generation of this Error frame,
|
17
12
|
# if applicable.
|
18
13
|
#
|
19
14
|
# This is a convenience method for:
|
20
|
-
# frame.headers[:message]
|
15
|
+
# frame.headers[:message]
|
21
16
|
def message
|
22
|
-
@headers
|
17
|
+
@headers[:message]
|
23
18
|
end
|
24
19
|
end
|
25
20
|
end
|
@@ -1,22 +1,17 @@
|
|
1
1
|
module Stomper
|
2
2
|
module Frames
|
3
3
|
# Encapsulates a "MESSAGE" server side frame for the Stomp Protocol.
|
4
|
-
#
|
5
|
-
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
-
# for more details.
|
7
4
|
class Message < Stomper::Frames::ServerFrame
|
8
|
-
# This class is the factory for all MESSAGE frames received.
|
9
|
-
factory_for :message
|
10
5
|
|
11
6
|
# Creates a new message frame with the given +headers+ and +body+
|
12
7
|
def initialize(headers, body)
|
13
|
-
super(
|
8
|
+
super(headers, body)
|
14
9
|
end
|
15
10
|
|
16
11
|
# Returns the message id generated by the stomp broker.
|
17
12
|
#
|
18
13
|
# This is a convenience method for:
|
19
|
-
# frame.headers[:'message-id']
|
14
|
+
# frame.headers[:'message-id']
|
20
15
|
def id
|
21
16
|
@headers[:'message-id']
|
22
17
|
end
|
@@ -24,20 +19,29 @@ module Stomper
|
|
24
19
|
# Returns the destination from which this message was delivered.
|
25
20
|
#
|
26
21
|
# This is a convenience method for:
|
27
|
-
# frame.headers.destination, frame.headers['destination'], or
|
28
22
|
# frame.headers[:destination]
|
29
23
|
def destination
|
30
|
-
@headers
|
24
|
+
@headers[:destination]
|
31
25
|
end
|
32
26
|
|
33
27
|
# Returns the name of the subscription which is responsible for the
|
34
28
|
# client having received this message.
|
35
29
|
#
|
36
30
|
# This is a convenience method for:
|
37
|
-
# frame.headers.subscription, frame.headers['subscription'] or
|
38
31
|
# frame.headers[:subscription]
|
39
32
|
def subscription
|
40
|
-
@headers
|
33
|
+
@headers[:subscription]
|
34
|
+
end
|
35
|
+
|
36
|
+
def perform
|
37
|
+
# TODO: I want the frames, particularly the server frames, to know
|
38
|
+
# 'what to do' when they are received. For instance, when a MESSAGE
|
39
|
+
# frame is received, the message should be applied to all
|
40
|
+
# stored subscriptions on the receiving connection. This will
|
41
|
+
# remove the conditional branching in the Client class, and provide
|
42
|
+
# a more flexible means of adding additional behaviors, instead of
|
43
|
+
# relying on what is sure to become a giant case statement in Client
|
44
|
+
# if we must change state on other Frames later.
|
41
45
|
end
|
42
46
|
end
|
43
47
|
end
|