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