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
@@ -1,16 +1,11 @@
1
1
  module Stomper
2
2
  module Frames
3
3
  # Encapsulates a "RECEIPT" 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 Receipt < Stomper::Frames::ServerFrame
8
- # This class is a factory for all RECEIPT commands received.
9
- factory_for :receipt
10
5
 
11
6
  # Creates a new Receipt frame with the supplied +headers+ and +body+
12
7
  def initialize(headers, body)
13
- super('RECEIPT', headers, body)
8
+ super(headers, body)
14
9
  end
15
10
 
16
11
  # Returns the 'receipt-id' header of the frame, which
@@ -1,13 +1,9 @@
1
1
  module Stomper
2
2
  module Frames
3
3
  # Encapsulates a "SEND" frame from the Stomp Protocol.
4
- #
5
- # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
- # for more details.
7
4
  class Send < Stomper::Frames::ClientFrame
8
5
  def initialize(destination, body, headers={})
9
- super('SEND', headers, body)
10
- @headers.destination = destination
6
+ super(headers.merge({ :destination => destination }), body)
11
7
  end
12
8
  end
13
9
  end
@@ -1,31 +1,21 @@
1
1
  module Stomper
2
2
  module Frames
3
3
  # Encapsulates a server side frame for the Stomp Protocol.
4
- #
5
- # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
- # for more details.
7
- class ServerFrame
8
- attr_reader :command, :headers, :body
4
+ class ServerFrame < IndirectFrame
9
5
 
10
6
  # Creates a new server frame corresponding to the
11
7
  # supplied +command+ with the given +headers+ and +body+.
12
- def initialize(command, headers={}, body=nil)
13
- @command = command
14
- @headers = Headers.new(headers)
15
- @body = body
8
+ def initialize(headers={}, body=nil, command = nil)
9
+ super
16
10
  end
17
11
 
18
12
  class << self
19
- # Provides a method for subclasses to register themselves
20
- # as factories for particular stomp commands by passing a list
21
- # of strings (or symbols) to this method. Each element in
22
- # the list is interpretted as the command for which we will
23
- # defer to the calling subclass to build.
24
- def factory_for(*args)
25
- @@registered_commands ||= {}
26
- args.each do |command|
27
- @@registered_commands[command.to_s.upcase] = self
28
- end
13
+ def inherited(server_frame) #:nodoc:
14
+ declared_frames << { :class => server_frame, :command => server_frame.name.split("::").last.downcase.to_sym }
15
+ end
16
+
17
+ def declared_frames
18
+ @declared_frames ||= []
29
19
  end
30
20
 
31
21
  # Builds a new ServerFrame instance by first checking to
@@ -35,11 +25,11 @@ module Stomper
35
25
  # ServerFrame instance is created with its +command+ attribute
36
26
  # set appropriately.
37
27
  def build(command, headers, body)
38
- command = command.to_s.upcase
39
- if @@registered_commands.has_key?(command)
40
- @@registered_commands[command].new(headers, body)
28
+ com_sym = command.downcase.to_sym
29
+ if klass = declared_frames.detect { |frame| com_sym == frame[:command] }
30
+ klass[:class].new(headers, body)
41
31
  else
42
- ServerFrame.new(command, headers, body)
32
+ ServerFrame.new(headers, body, command)
43
33
  end
44
34
  end
45
35
  end
@@ -1,46 +1,41 @@
1
1
  module Stomper
2
2
  module Frames
3
3
  # Encapsulates a "SUBSCRIBE" frame from the Stomp Protocol.
4
- #
5
- # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
- # for more details.
7
4
  class Subscribe < Stomper::Frames::ClientFrame
8
5
  def initialize(destination, headers={})
9
- super('SUBSCRIBE', headers)
10
- @headers['destination'] = destination
11
- @headers['ack'] ||= 'auto'
6
+ super({ :ack => 'auto' }.merge(headers).merge({ :destination => destination }))
12
7
  end
13
8
 
14
9
  # Returns the ack mode of this subscription. (defaults to 'auto')
15
10
  #
16
11
  # This is a convenience method, and may also be accessed through
17
- # frame.headers.ack or frame.headers[:ack] or frame.headers['ack']
12
+ # frame.headers[:ack]
18
13
  def ack
19
- @headers['ack']
14
+ @headers[:ack]
20
15
  end
21
16
 
22
17
  # Returns the destination to which we are subscribing.
23
18
  #
24
19
  # This is a convenience method, and may also be accessed through
25
- # frame.headers.destination or frame.headers[:destination] or frame.headers['destination']
20
+ # frame.headers[:destination]
26
21
  def destination
27
- @headers['destination']
22
+ @headers[:destination]
28
23
  end
29
24
 
30
25
  # Returns the id of this subscription, if it has been set.
31
26
  #
32
27
  # This is a convenience method, and may also be accessed through
33
- # frame.headers.id or frame.headers[:id] or frame.headers['id']
28
+ # frame.headers[:id]
34
29
  def id
35
- @headers['id']
30
+ @headers[:id]
36
31
  end
37
32
 
38
33
  # Returns the selector header of this subscription, if it has been set.
39
34
  #
40
35
  # This is a convenience method, and may also be accessed through
41
- # frame.headers.selector or frame.headers[:selector] or frame.headers['selector']
36
+ # frame.headers[:selector]
42
37
  def selector
43
- @headers['selector']
38
+ @headers[:selector]
44
39
  end
45
40
  end
46
41
  end
@@ -1,22 +1,18 @@
1
1
  module Stomper
2
2
  module Frames
3
3
  # Encapsulates an "UNSUBSCRIBE" frame from the Stomp Protocol.
4
- #
5
- # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
- # for more details.
7
4
  class Unsubscribe < Stomper::Frames::ClientFrame
8
5
  def initialize(destination, headers={})
9
- super('UNSUBSCRIBE', headers)
10
- @headers['destination'] = destination
6
+ super(headers.merge({ :destination => destination }))
11
7
  end
12
8
 
13
9
  # Returns the id of the subscription being unsubscribed from, if it
14
10
  # exists.
15
11
  #
16
12
  # This is a convenience method, and may also be accessed through
17
- # frame.headers.id or frame.headers[:id] or frame.headers['id']
13
+ # frame.headers[:id]
18
14
  def id
19
- @headers['id']
15
+ @headers[:id]
20
16
  end
21
17
  end
22
18
  end
@@ -0,0 +1,41 @@
1
+ module Stomper
2
+ module OpenUriInterface
3
+ def put(msg, headers={})
4
+ send(default_destination, msg, headers.merge(:generate_content_length => false))
5
+ end
6
+ alias_method :puts, :put
7
+
8
+ def write(msg, headers={})
9
+ send(default_destination, msg, headers.merge(:generate_content_length => true))
10
+ end
11
+
12
+ def first(n=1)
13
+ received = []
14
+ each do |m|
15
+ received << m
16
+ break if received.size == n
17
+ end
18
+ n == 1 ? received.first : received
19
+ end
20
+ alias_method :get, :first
21
+ alias_method :gets, :first
22
+ alias_method :read, :first
23
+
24
+ # This is the tricky one.
25
+ # The subscriber interface is not going to work here, because it is built
26
+ # for an entirely different use case (threaded receiving)
27
+ # This interface, by contrast, is blocking... fudge.
28
+ def each(&block)
29
+ subscription = subscribe(default_destination) { |m| m }
30
+ loop do
31
+ m = receive
32
+ yield m if m.is_a?(Stomper::Frames::Message) && subscription.accepts?(m)
33
+ end
34
+ end
35
+
36
+ private
37
+ def default_destination
38
+ uri.path
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Stomper
2
+ class ReceiptHandlers
3
+ def initialize
4
+ @recps = {}
5
+ @recp_lock = Mutex.new
6
+ end
7
+
8
+ def add(receipt_id, callback)
9
+ @recp_lock.synchronize { @recps[receipt_id] = callback }
10
+ end
11
+
12
+ def size
13
+ @recps.size
14
+ end
15
+
16
+ def perform(receipt)
17
+ @recp_lock.synchronize do
18
+ callback = @recps.delete(receipt.for)
19
+ callback.call(receipt) if callback
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Stomper
2
+ module Receiptor
3
+ def self.included(base)
4
+ if base.method_defined?(:receive)
5
+ base.instance_eval do
6
+ alias_method :receive_without_receipt_dispatch, :receive
7
+ alias_method :receive, :receive_with_receipt_dispatch
8
+ end
9
+ end
10
+ if base.method_defined?(:send)
11
+ base.instance_eval do
12
+ alias_method :send_without_receipt_handler, :send
13
+ alias_method :send, :send_with_receipt_handler
14
+ end
15
+ end
16
+ end
17
+
18
+ # Receives a frame and dispatches it to the known receipt handlers, if the
19
+ # received frame is a RECEIPT frame.
20
+ def receive_with_receipt_dispatch
21
+ frame = receive_without_receipt_dispatch
22
+ receipt_handlers.perform(frame) if frame.is_a?(::Stomper::Frames::Receipt)
23
+ frame
24
+ end
25
+
26
+ def send_with_receipt_handler(destination, body, headers={}, &block)
27
+ if block_given?
28
+ headers[:receipt] ||= "rcpt-#{Time.now.to_f}"
29
+ receipt_handlers.add(headers[:receipt], block)
30
+ end
31
+ send_without_receipt_handler(destination, body, headers)
32
+ end
33
+
34
+ def receipt_handlers
35
+ @receipt_handlers ||= ::Stomper::ReceiptHandlers.new
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module Stomper
2
+ module Sockets
3
+ class SSL < DelegateClass(OpenSSL::SSL::SSLSocket)
4
+ include FrameReader
5
+ include FrameWriter
6
+
7
+ def initialize(host, port, *args)
8
+ @context = OpenSSL::SSL::SSLContext.new
9
+ @context.verify_mode = OpenSSL::SSL::VERIFY_NONE
10
+
11
+ tcp_sock = TCPSocket.new(host, port)
12
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_sock, @context)
13
+ @socket.sync_close = true
14
+ @socket.connect
15
+ super(@socket)
16
+ end
17
+
18
+ def ready?
19
+ @socket.io.ready?
20
+ end
21
+
22
+ def shutdown(mode=2)
23
+ @socket.io.shutdown(mode)
24
+ end
25
+ end
26
+
27
+ class TCP < DelegateClass(TCPSocket)
28
+ include FrameReader
29
+ include FrameWriter
30
+
31
+ def initialize(host, port, *args)
32
+ @socket = TCPSocket.new(host, port)
33
+ super(@socket)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,76 @@
1
+ module Stomper
2
+ module Subscriber
3
+ def self.included(base)
4
+ if base.method_defined?(:receive)
5
+ base.instance_eval do
6
+ alias_method :receive_without_message_dispatch, :receive
7
+ alias_method :receive, :receive_with_message_dispatch
8
+ end
9
+ end
10
+ end
11
+
12
+ # Receives a frame and dispatches it to the known subscriptions, if the
13
+ # received frame is a MESSAGE frame.
14
+ def receive_with_message_dispatch
15
+ frame = receive_without_message_dispatch
16
+ subscriptions.perform(frame) if frame.is_a?(::Stomper::Frames::Message)
17
+ frame
18
+ end
19
+
20
+ # Subscribes to the specified +destination+, passing along
21
+ # the optional +headers+ inside the subscription frame. When a message
22
+ # is received for this subscription, the supplied +block+ is
23
+ # called with the received message as its argument.
24
+ #
25
+ # Examples:
26
+ #
27
+ # client.subscribe("/queue/test") { |msg| puts "Got message: #{msg.body}" }
28
+ #
29
+ # client.subscribe("/queue/test", :ack => 'client', 'id' => 'subscription-001') do |msg|
30
+ # puts "Got message: #{msg.body}"
31
+ # end
32
+ #
33
+ # client.subscribe("/queue/test", :selector => 'cost > 5') do |msg|
34
+ # puts "Got message: #{msg.body}"
35
+ # end
36
+ #
37
+ # See also: unsubscribe, Stomper::Subscription
38
+ def subscribe(destination, headers={}, &block)
39
+ unless destination.is_a?(Subscription)
40
+ destination = Subscription.new(headers.merge(:destination => destination), &block)
41
+ end
42
+ subscriptions << destination
43
+ transmit(destination.to_subscribe)
44
+ destination
45
+ end
46
+
47
+ # Unsubscribes from the specified +destination+. The +destination+
48
+ # parameter may be either a string, such as "/queue/test", or Stomper::Subscription
49
+ # object. If the optional +sub_id+ is supplied, the client will unsubscribe
50
+ # from the subscription with an id matching +sub_id+, regardless if the
51
+ # +destination+ parameter matches that of the registered subscription. For
52
+ # this reason, it is vital that subscription ids, if manually specified, be
53
+ # unique.
54
+ #
55
+ # Examples:
56
+ #
57
+ # client.unsubscribe("/queue/test")
58
+ # # unsubscribes from all "naive" subscriptions for "/queue/test"
59
+ #
60
+ # client.unsubscribe("/queue/does/not/matter", "sub-0013012031")
61
+ # # unsubscribes from all subscriptions with id of "sub-0013012031"
62
+ #
63
+ # client.unsubscribe(some_subscription)
64
+ #
65
+ # See also: subscribe, Stomper::Subscription
66
+ def unsubscribe(destination, sub_id=nil)
67
+ subscriptions.remove(destination, sub_id).each do |unsub|
68
+ transmit(unsub.to_unsubscribe)
69
+ end
70
+ end
71
+
72
+ def subscriptions
73
+ @subscriptions ||= ::Stomper::Subscriptions.new
74
+ end
75
+ end
76
+ end
@@ -36,23 +36,23 @@ module Stomper
36
36
  #
37
37
  # If no +subscription_id+ is specified, either explicitly or through a
38
38
  # hash key of 'id' in +destination_or_options+, one may be automatically
39
- # generated of the form "sub-#{Time.now.to_f}". The automatic generation
39
+ # generated of the form +sub-<Time.now.to_f>+. The automatic generation
40
40
  # of a subscription id occurs if and only if naive? returns false.
41
41
  #
42
42
  # While direct creation of Subscription instances is possible, the preferred
43
43
  # method is for them to be constructed by a Stomper::Client through the use
44
44
  # of the Stomper::Client#subscribe method.
45
45
  #
46
- # See also: naive?, Stomper::Client#subscribe, Stomper::Client#unsubscribe,
47
- # Stomper::Client#ack
48
- #
46
+ # @see naive?
47
+ # @see Subscriber#subscribe
48
+ # @see Subscriber#unsubscribe
49
+ # @see Client#ack
49
50
  def initialize(destination_or_options, subscription_id=nil, ack=nil, selector=nil, &block)
50
51
  if destination_or_options.is_a?(Hash)
51
- options = Stomper::Frames::Headers.new(destination_or_options)
52
- destination = options.destination
53
- subscription_id ||= options.id
54
- ack ||= options.ack
55
- selector ||= options.selector
52
+ destination = destination_or_options[:destination]
53
+ subscription_id ||= destination_or_options[:id]
54
+ ack ||= destination_or_options[:ack]
55
+ selector ||= destination_or_options[:selector]
56
56
  else
57
57
  destination = destination_or_options.to_s
58
58
  end
@@ -110,9 +110,9 @@ module Stomper
110
110
  # Stomper::Frames::Subscribe client frame that can be transmitted
111
111
  # to a stomp broker through a Stomper::Connection instance.
112
112
  def to_subscribe
113
- headers = { 'destination' => @destination, 'ack' => @ack.to_s }
114
- headers['id'] = @id unless @id.nil?
115
- headers['selector'] = @selector unless @selector.nil?
113
+ headers = { :destination => @destination, :ack => @ack.to_s }
114
+ headers[:id] = @id unless @id.nil?
115
+ headers[:selector] = @selector unless @selector.nil?
116
116
  Stomper::Frames::Subscribe.new(@destination, headers)
117
117
  end
118
118
 
@@ -120,8 +120,8 @@ module Stomper
120
120
  # Stomper::Frames::Unsubscribe client frame that can be transmitted
121
121
  # to a stomp broker through a Stomper::Connection instance.
122
122
  def to_unsubscribe
123
- headers = { 'destination' => @destination }
124
- headers['id'] = @id unless @id.nil?
123
+ headers = { :destination => @destination }
124
+ headers[:id] = @id unless @id.nil?
125
125
  Stomper::Frames::Unsubscribe.new(@destination, headers)
126
126
  end
127
127
  end