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.
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