stomper 0.4 → 1.0.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/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
|