stomper 0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/CHANGELOG +7 -0
  2. data/README.rdoc +63 -11
  3. data/lib/stomper.rb +13 -1
  4. data/lib/stomper/client.rb +3 -284
  5. data/lib/stomper/connection.rb +67 -114
  6. data/lib/stomper/frame_reader.rb +73 -0
  7. data/lib/stomper/frame_writer.rb +21 -0
  8. data/lib/stomper/frames.rb +24 -9
  9. data/lib/stomper/frames/abort.rb +2 -6
  10. data/lib/stomper/frames/ack.rb +2 -6
  11. data/lib/stomper/frames/begin.rb +2 -5
  12. data/lib/stomper/frames/client_frame.rb +30 -27
  13. data/lib/stomper/frames/commit.rb +1 -5
  14. data/lib/stomper/frames/connect.rb +2 -7
  15. data/lib/stomper/frames/connected.rb +11 -8
  16. data/lib/stomper/frames/disconnect.rb +1 -4
  17. data/lib/stomper/frames/error.rb +3 -8
  18. data/lib/stomper/frames/message.rb +15 -11
  19. data/lib/stomper/frames/receipt.rb +1 -6
  20. data/lib/stomper/frames/send.rb +1 -5
  21. data/lib/stomper/frames/server_frame.rb +13 -23
  22. data/lib/stomper/frames/subscribe.rb +9 -14
  23. data/lib/stomper/frames/unsubscribe.rb +3 -7
  24. data/lib/stomper/open_uri_interface.rb +41 -0
  25. data/lib/stomper/receipt_handlers.rb +23 -0
  26. data/lib/stomper/receiptor.rb +38 -0
  27. data/lib/stomper/sockets.rb +37 -0
  28. data/lib/stomper/subscriber.rb +76 -0
  29. data/lib/stomper/subscription.rb +14 -14
  30. data/lib/stomper/threaded_receiver.rb +59 -0
  31. data/lib/stomper/transaction.rb +13 -8
  32. data/lib/stomper/transactor.rb +50 -0
  33. data/lib/stomper/uri.rb +55 -0
  34. data/spec/client_spec.rb +7 -158
  35. data/spec/connection_spec.rb +13 -3
  36. data/spec/frame_reader_spec.rb +37 -0
  37. data/spec/frame_writer_spec.rb +27 -0
  38. data/spec/frames/client_frame_spec.rb +22 -98
  39. data/spec/frames/indirect_frame_spec.rb +45 -0
  40. data/spec/frames/server_frame_spec.rb +15 -16
  41. data/spec/open_uri_interface_spec.rb +132 -0
  42. data/spec/receiptor_spec.rb +35 -0
  43. data/spec/shared_connection_examples.rb +12 -17
  44. data/spec/spec_helper.rb +6 -0
  45. data/spec/subscriber_spec.rb +77 -0
  46. data/spec/subscription_spec.rb +11 -11
  47. data/spec/subscriptions_spec.rb +3 -6
  48. data/spec/threaded_receiver_spec.rb +33 -0
  49. data/spec/transaction_spec.rb +5 -5
  50. data/spec/transactor_spec.rb +46 -0
  51. metadata +30 -6
  52. data/lib/stomper/frames/headers.rb +0 -68
  53. 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
@@ -1,4 +1,27 @@
1
- require 'stomper/frames/headers'
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
@@ -1,13 +1,9 @@
1
1
  module Stomper
2
2
  module Frames
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.
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('ABORT', headers)
10
- @headers.transaction = transaction_id
6
+ super(headers.merge(:transaction => transaction_id))
11
7
  end
12
8
  end
13
9
  end
@@ -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('ACK', headers)
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['transaction'] = message.headers.transaction if message.headers.transaction
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)
@@ -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('BEGIN', headers)
10
- @headers.transaction = transaction_id
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(command, headers={}, body=nil)
18
- @command = command
13
+ def initialize(headers={}, body=nil, command = nil)
19
14
  @generate_content_length = headers.delete(:generate_content_length)
20
- @headers = Headers.new(headers)
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
- # Converts the frame instance into a valid string representation of the
42
- # desired command according to the specifications of the
43
- # {Stomp Protocol}[http://stomp.codehaus.org/Protocol]
44
- #
45
- # This is where the content-length header is generated if the frame
46
- # has a body of non-zero length and generate_content_length? is true.
47
- def to_stomp
48
- @headers["content-length"] = str_size(@body) if @body && !@body.empty? && generate_content_length?
49
- "#{@command}\n#{@headers.to_stomp}\n#{@body}\0"
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 to_stomp method is: check the instance setting,
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
- def str_size(str)
76
- if str.respond_to?(:bytesize)
77
- def str_size(strng); strng.bytesize; end
78
- str.bytesize
79
- else
80
- def str_size(strng); strng.size; end
81
- str.size
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('COMMIT', headers)
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(username='', password='', headers={})
9
- super('CONNECT', headers)
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('CONNECTED', headers, body)
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.session or frame.headers['session'] or frame.headers[:session]
16
+ # frame.headers[:session]
22
17
  def session
23
- @headers.session
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('DISCONNECT', headers)
6
+ super(headers)
10
7
  end
11
8
  end
12
9
  end
@@ -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('ERROR', headers, body)
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], frame.headers['message'], or frame.headers.message
15
+ # frame.headers[:message]
21
16
  def message
22
- @headers.message
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('MESSAGE', headers, body)
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'] or 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.destination
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.subscription
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