onstomp 1.0.0pre1

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 (80) hide show
  1. data/.autotest +2 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +4 -0
  6. data/DeveloperNarrative.md +15 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.md +221 -0
  9. data/README.md +73 -0
  10. data/Rakefile +6 -0
  11. data/UserNarrative.md +8 -0
  12. data/examples/basic.rb +40 -0
  13. data/examples/events.rb +72 -0
  14. data/lib/onstomp/client.rb +152 -0
  15. data/lib/onstomp/components/frame.rb +108 -0
  16. data/lib/onstomp/components/frame_headers.rb +212 -0
  17. data/lib/onstomp/components/nil_processor.rb +20 -0
  18. data/lib/onstomp/components/scopes/header_scope.rb +25 -0
  19. data/lib/onstomp/components/scopes/receipt_scope.rb +25 -0
  20. data/lib/onstomp/components/scopes/transaction_scope.rb +191 -0
  21. data/lib/onstomp/components/scopes.rb +45 -0
  22. data/lib/onstomp/components/subscription.rb +30 -0
  23. data/lib/onstomp/components/threaded_processor.rb +62 -0
  24. data/lib/onstomp/components/uri.rb +30 -0
  25. data/lib/onstomp/components.rb +13 -0
  26. data/lib/onstomp/connections/base.rb +208 -0
  27. data/lib/onstomp/connections/heartbeating.rb +82 -0
  28. data/lib/onstomp/connections/serializers/stomp_1.rb +166 -0
  29. data/lib/onstomp/connections/serializers/stomp_1_0.rb +41 -0
  30. data/lib/onstomp/connections/serializers/stomp_1_1.rb +134 -0
  31. data/lib/onstomp/connections/serializers.rb +9 -0
  32. data/lib/onstomp/connections/stomp_1.rb +69 -0
  33. data/lib/onstomp/connections/stomp_1_0.rb +28 -0
  34. data/lib/onstomp/connections/stomp_1_1.rb +65 -0
  35. data/lib/onstomp/connections.rb +119 -0
  36. data/lib/onstomp/interfaces/client_configurable.rb +55 -0
  37. data/lib/onstomp/interfaces/client_events.rb +168 -0
  38. data/lib/onstomp/interfaces/connection_events.rb +62 -0
  39. data/lib/onstomp/interfaces/event_manager.rb +69 -0
  40. data/lib/onstomp/interfaces/frame_methods.rb +190 -0
  41. data/lib/onstomp/interfaces/receipt_manager.rb +33 -0
  42. data/lib/onstomp/interfaces/subscription_manager.rb +48 -0
  43. data/lib/onstomp/interfaces/uri_configurable.rb +106 -0
  44. data/lib/onstomp/interfaces.rb +14 -0
  45. data/lib/onstomp/version.rb +13 -0
  46. data/lib/onstomp.rb +147 -0
  47. data/onstomp.gemspec +29 -0
  48. data/spec/onstomp/client_spec.rb +265 -0
  49. data/spec/onstomp/components/frame_headers_spec.rb +163 -0
  50. data/spec/onstomp/components/frame_spec.rb +144 -0
  51. data/spec/onstomp/components/nil_processor_spec.rb +32 -0
  52. data/spec/onstomp/components/scopes/header_scope_spec.rb +27 -0
  53. data/spec/onstomp/components/scopes/receipt_scope_spec.rb +33 -0
  54. data/spec/onstomp/components/scopes/transaction_scope_spec.rb +227 -0
  55. data/spec/onstomp/components/scopes_spec.rb +63 -0
  56. data/spec/onstomp/components/subscription_spec.rb +58 -0
  57. data/spec/onstomp/components/threaded_processor_spec.rb +92 -0
  58. data/spec/onstomp/components/uri_spec.rb +33 -0
  59. data/spec/onstomp/connections/base_spec.rb +349 -0
  60. data/spec/onstomp/connections/heartbeating_spec.rb +132 -0
  61. data/spec/onstomp/connections/serializers/stomp_1_0_spec.rb +50 -0
  62. data/spec/onstomp/connections/serializers/stomp_1_1_spec.rb +99 -0
  63. data/spec/onstomp/connections/serializers/stomp_1_spec.rb +104 -0
  64. data/spec/onstomp/connections/stomp_1_0_spec.rb +54 -0
  65. data/spec/onstomp/connections/stomp_1_1_spec.rb +137 -0
  66. data/spec/onstomp/connections/stomp_1_spec.rb +113 -0
  67. data/spec/onstomp/connections_spec.rb +135 -0
  68. data/spec/onstomp/interfaces/client_events_spec.rb +108 -0
  69. data/spec/onstomp/interfaces/connection_events_spec.rb +55 -0
  70. data/spec/onstomp/interfaces/event_manager_spec.rb +72 -0
  71. data/spec/onstomp/interfaces/frame_methods_spec.rb +109 -0
  72. data/spec/onstomp/interfaces/receipt_manager_spec.rb +53 -0
  73. data/spec/onstomp/interfaces/subscription_manager_spec.rb +64 -0
  74. data/spec/onstomp_spec.rb +15 -0
  75. data/spec/spec_helper.rb +12 -0
  76. data/spec/support/custom_argument_matchers.rb +51 -0
  77. data/spec/support/frame_matchers.rb +88 -0
  78. data/spec/support/shared_frame_method_examples.rb +116 -0
  79. data/yard_extensions.rb +32 -0
  80. metadata +219 -0
@@ -0,0 +1,108 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A generic encapsulation of a frame as specified by the Stomp protocol.
4
+ class OnStomp::Components::Frame
5
+ # Regex to match content-type header value.
6
+ # Eg: given "text/plain; ... ;charset=ISO-8859-1 ...", then
7
+ # * $1 => type ('text')
8
+ # * $2 => subtype ('plain')
9
+ # * $3 => charset ('ISO-8859-1')
10
+ CONTENT_TYPE_REG = /^([a-z0-9!\#$&.+\-^_]+)\/([a-z0-9!\#$&.+\-^_]+)(?:.*;\s*charset=\"?([a-z0-9!\#$&.+\-^_]+)\"?)?/i
11
+
12
+ attr_accessor :command, :body
13
+ attr_reader :headers
14
+
15
+ # Creates a new frame. The frame will be initialized with the optional
16
+ # +command+ name, a {OnStomp::Components::FrameHeaders headers} collection initialized
17
+ # with the optional +headers+ hash, and an optional body.
18
+ def initialize(command=nil, headers={}, body=nil)
19
+ @command = command
20
+ @headers = OnStomp::Components::FrameHeaders.new(headers)
21
+ @body = body
22
+ end
23
+
24
+ # Gets the header value paired with the supplied name. This is a convenient
25
+ # shortcut for `frame.headers[name]`.
26
+ #
27
+ # @param [Object] name the header name associated with the desired value
28
+ # @return [String] the value associated with the requested header name
29
+ # @see OnStomp::Headers#[]
30
+ # @example
31
+ # frame['content-type'] #=> 'text/plain'
32
+ def [](name); @headers[name]; end
33
+
34
+ # Sets the header value paired with the supplied name. This is a convenient
35
+ # shortcut for `frame.headers[name] = val`.
36
+ #
37
+ # @param [Object] name the header name to associate with the supplied value
38
+ # @param [Object] val the value to associate with the supplied header name
39
+ # @return [String] the supplied value as a string, or `nil` if `nil` was supplied as the value.
40
+ # @see OnStomp::Headers#[]=
41
+ # @example
42
+ # frame['content-type'] = 'text/plain' #=> 'text/plain'
43
+ # frame['other header'] = 42 #=> '42'
44
+ def []=(name, val); @headers[name] = val; end
45
+
46
+ # If a +content-length+ header is set, returns it after converting it to
47
+ # an integer.
48
+ # @return [Fixnum, nil]
49
+ def content_length
50
+ header?(:'content-length') ? @headers[:'content-length'].to_i : nil
51
+ end
52
+
53
+ # If a +content-type+ header is set, splits it into three parts: type,
54
+ # subtype and charset. If any component of the +content-type+ is missing,
55
+ # its value will be +nil+ in the returned triple. If the +content-type+
56
+ # header is not set or does not match {OnStomp::Components::Frame::CONTENT_TYPE_REG}
57
+ # all values in the triple will be +nil+.
58
+ # @return [Array<String,nil>]
59
+ def content_type
60
+ @headers[:'content-type'] =~ CONTENT_TYPE_REG ? [$1, $2, $3] : [nil, nil, nil]
61
+ end
62
+
63
+ # Returns true if the given header name exists and its value is not an
64
+ # empty string.
65
+ # @param [#to_sym] name
66
+ # @return [true,false]
67
+ # @see OnStomp::Components::FrameHeaders#present?
68
+ def header? name
69
+ @headers.present? name
70
+ end
71
+
72
+ # Returns true if all given header names exist and none of their values are
73
+ # empty strings.
74
+ def all_headers? *names
75
+ names.all? { |name| @headers.present?(name) }
76
+ end
77
+ alias :headers? :all_headers?
78
+
79
+ # Returns the heart-beat configuration specified in this frame's headers.
80
+ # If a +heart-beat+ header is not set, [0, 0] will be returned. Otherwise,
81
+ # the header value will be split on ',' and each component will be converted
82
+ # to a non-negative integer.
83
+ # @return [[Fixnum,Fixnum]] pair of non-negative integers that specify
84
+ # connection heart-beat settings
85
+ def heart_beat
86
+ (@headers[:'heart-beat'] || '0,0').split(',').map do |v|
87
+ vi = v.to_i
88
+ vi > 0 ? vi : 0
89
+ end
90
+ end
91
+
92
+ # Sets this frame's +content-length+ header to match the byte-length of
93
+ # its body, if the body has been set.
94
+ # @return <Fixnum,nil>
95
+ def force_content_length
96
+ @headers[:'content-length'] = body_length if body
97
+ end
98
+
99
+ if RUBY_VERSION >= "1.9"
100
+ # Returns the byte-length of this frame's body
101
+ # @return [Fixnum]
102
+ def body_length; body.bytesize; end
103
+ else
104
+ # Returns the byte-length of this frame's body
105
+ # @return [Fixnum]
106
+ def body_length; body.length; end
107
+ end
108
+ end
@@ -0,0 +1,212 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A specialized container for storing header name / value pairs for a
4
+ # {OnStomp::Components::Frame frame}. This container behaves much like a +Hash+, but
5
+ # is specialized for the Stomp protocol. Header names are always converted
6
+ # into +String+s through the use of +to_s+ and may have more than one value
7
+ # associated with them.
8
+ class OnStomp::Components::FrameHeaders
9
+ include Enumerable
10
+
11
+ # Creates a new headers collection, initialized with the optional hash
12
+ # parameter.
13
+ # @param [Hash] headers
14
+ # @see #merge!
15
+ def initialize(headers={})
16
+ @values = {}
17
+ __initialize_names__
18
+ merge! headers
19
+ end
20
+
21
+ # Merges a hash into this collection of headers. All of the keys used
22
+ # in the hash must be convertable to Symbols through +to_sym+.
23
+ # @note With Ruby 1.8.7, the order of hash keys may not be preserved
24
+ # @param [Hash] hash
25
+ def merge!(hash)
26
+ hash.each { |k, v| self[k]= v }
27
+ end
28
+
29
+ # Reverse merges a hash into this collection of headers. The hash keys and
30
+ # values are included only if the headers collection does not already have
31
+ # a matching key. All of the keys used
32
+ # in the hash must be convertable to Symbols through +to_sym+.
33
+ # @note With Ruby 1.8.7, the order of hash keys may not be preserved
34
+ # @param [Hash] hash
35
+ def reverse_merge!(hash)
36
+ hash.each { |k, v|
37
+ self[k]= v unless set?(k)
38
+ }
39
+ end
40
+
41
+ # Returns true if a header value has been set for the supplied header name.
42
+ # @param [#to_sym] name the header name to test
43
+ # @return [Boolean]
44
+ # @example
45
+ # header.set? 'content-type' #=> true
46
+ def set?(name)
47
+ @values.key?(name.to_sym)
48
+ end
49
+
50
+ # Returns true if a header value has been set for the supplied header, and
51
+ # the value is neither +nil+ nor an empty string.
52
+ # @param [#to_sym] name the header name to test
53
+ # @return [Boolean]
54
+ # @example
55
+ # header[:test1] = 'set'
56
+ # header[:test2] = ''
57
+ # header.present? :test1 #=> true
58
+ # header.present? :test2 #=> false
59
+ def present?(name)
60
+ val = self[name]
61
+ !(val.nil? || val.empty?)
62
+ end
63
+
64
+ # Retrieves all header values associated with the supplied header name.
65
+ # In general, this will be an array containing only the principle header
66
+ # value; however, in the event a frame contained repeated header names,
67
+ # this method will return all of the associated values. The first
68
+ # element of the array will be the principle value of the supplied
69
+ # header name.
70
+ #
71
+ # @param [#to_sym] name the header name associated with the desired values (will be converted using +to_sym+)
72
+ # @return [Array] the array of values associated with the header name.
73
+ # @example
74
+ # headers.all_values('content-type') #=> [ 'text/plain' ]
75
+ # headers.all_values(:repeated_header) #=> [ 'principle value', '13', 'other value']
76
+ # headers['name'] == headers.all_values(:name).first #=> true
77
+ def all_values(name)
78
+ @values[name.to_sym] || []
79
+ end
80
+
81
+ # Appends a header value to the specified header name. If the specified
82
+ # header name is not known, the supplied value will also become the
83
+ # principle value. This method is used internally when constructing
84
+ # frames sent by the broker to capture repeated header names.
85
+ #
86
+ # @param [#to_sym] name the header name to associate with the supplied value (will be converted using +to_s+)
87
+ # @param [#to_s] val the header value to associate with the supplied name (will be converted using +to_s+)
88
+ # @return [String] the supplied value as a string.
89
+ # @example
90
+ # headers.append(:'new header', 'first value') #=> 'first value'
91
+ # headers.append('new header', nil) #=> ''
92
+ # headers.append('new header', 13) #=> '13'
93
+ # headers['new header'] #=> 'first value'
94
+ # headers.all('new header') #=> ['first value', '', '13']
95
+ def append(name, val)
96
+ name = name.to_sym
97
+ val = val.to_s
98
+ if @values.key?(name)
99
+ @values[name] << val
100
+ else
101
+ self[name]= val
102
+ end
103
+ val
104
+ end
105
+
106
+ # Deletes all of the header values associated with the header name and
107
+ # removes the header name itself. This is analogous to the +delete+
108
+ # method found in Hash objects.
109
+ #
110
+ # @param [#to_sym] name the header name to remove from this collection (will be converted using +to_sym+)
111
+ # @return [Array] the array of values associated with the deleted header, or +nil+ if the header name did not exist
112
+ # @example
113
+ # headers.delete(:'content-type') #=> [ 'text/html' ]
114
+ # headers.delete('no such header') #=> nil
115
+ def delete(name)
116
+ name = name.to_sym
117
+ if @values.key? name
118
+ __delete_name__ name
119
+ @values.delete name
120
+ end
121
+ end
122
+
123
+ # Gets the principle header value paired with the supplied header name. The name will
124
+ # be converted to a Symbol, so must respond to the +to_sym+ method. The
125
+ # Stomp 1.1 protocol specifies that in the event of a repeated header name,
126
+ # the first value encountered serves as the principle value.
127
+ #
128
+ # @param [#to_sym] name the header name paired with the desired value (will be converted using +to_sym+)
129
+ # @return [String] the value associated with the requested header name
130
+ # @return [nil] if no value has been set for the associated header name
131
+ # @example
132
+ # headers['content-type'] #=> 'text/plain'
133
+ def [](name)
134
+ name = name.to_sym
135
+ @values[name] && @values[name].first
136
+ end
137
+
138
+ # Sets the header value paired with the supplied header name. The name
139
+ # will be converted to a Symbol and must respond to +to_sym+; meanwhile,
140
+ # the value will be converted to a String so must respond to +to_s+.
141
+ # Setting a header value in this fashion will overwrite any repeated header values.
142
+ #
143
+ # @param [#to_sym] name the header name to associate with the supplied value (will be converted using +to_sym+)
144
+ # @param [#to_s] val the value to pair with the supplied name (will be converted using +to_s+)
145
+ # @return [String] the supplied value as a string.
146
+ # @example
147
+ # headers['content-type'] = 'image/png' #=> 'image/png'
148
+ # headers[:'content-type'] = nil #=> ''
149
+ # headers['content-type'] #=> ''
150
+ def []=(name, val)
151
+ name = name.to_sym
152
+ val = val.to_s
153
+ __add_name__ name
154
+ @values[name] = [val]
155
+ val
156
+ end
157
+
158
+ # Returns a new +Hash+ object associating symbolized header names and their
159
+ # principle values.
160
+ # @return [Hash]
161
+ def to_hash
162
+ @values.inject({}) { |h, (k,v)| h[k] = v.first; h }
163
+ end
164
+
165
+ if RUBY_VERSION >= "1.9"
166
+ def names; @values.keys; end
167
+
168
+ def each(&block)
169
+ if block_given?
170
+ @values.each do |name, vals|
171
+ name_str = name.to_s
172
+ vals.each do |val|
173
+ yield [name_str, val]
174
+ end
175
+ end
176
+ self
177
+ else
178
+ Enumerator.new(self)
179
+ end
180
+ end
181
+
182
+ private
183
+ def __initialize_names__; end
184
+ def __delete_name__(name); end
185
+ def __add_name__(name); end
186
+ else
187
+ attr_reader :names
188
+
189
+ # Iterates over header name / value pairs, yielding them as a pair
190
+ # of strings to the supplied block.
191
+ # @yield [header_name, header_value]
192
+ # @yieldparam [String] header_name
193
+ # @yieldparam [String] header_value
194
+ def each(&block)
195
+ if block_given?
196
+ @names.each do |name|
197
+ @values[name].each do |val|
198
+ yield [name.to_s, val]
199
+ end
200
+ end
201
+ self
202
+ else
203
+ Enumerable::Enumerator.new(self)
204
+ end
205
+ end
206
+
207
+ private
208
+ def __initialize_names__; @names = []; end
209
+ def __delete_name__(name); @names.delete name; end
210
+ def __add_name__(name); @names << name unless @values.key?(name); end
211
+ end
212
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A processor that does nothing, used in the event that
4
+ # {OnStomp::Client client} +processor+ attribute is set to +nil+
5
+ class OnStomp::Components::NilProcessor
6
+ # Creates a new processor
7
+ def initialize(client); end
8
+ # Always returns +false+
9
+ # @return [false]
10
+ def running?; false; end
11
+ # Does nothing
12
+ # @return [self]
13
+ def start; self; end
14
+ # Does nothing
15
+ # @return [self]
16
+ def join; self; end
17
+ # Does nothing
18
+ # @return [self]
19
+ def stop; self; end
20
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Adds the a set of headers to all frames generated on the scope.
4
+ class OnStomp::Components::Scopes::HeaderScope
5
+ include OnStomp::Interfaces::FrameMethods
6
+
7
+ attr_reader :headers, :client, :connection
8
+
9
+ def initialize headers, client
10
+ @headers = headers
11
+ @client = client
12
+ @connection = client.connection
13
+ end
14
+
15
+ # Wraps {OnStomp::Client#transmit}, applying the set of {#headers} to
16
+ # all frames befor they are delivered to the broker.
17
+ # @param [OnStomp::Components::Frame] frame
18
+ # @param [{Symbol => Proc}] cbs
19
+ # @return [OnStomp::Components::Frame]
20
+ # @see OnStomp::Client#transmit
21
+ def transmit frame, cbs={}
22
+ frame.headers.reverse_merge!(headers)
23
+ client.transmit frame, cbs
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Adds a receipt callback to all receipt-able frames generated on this scope
4
+ class OnStomp::Components::Scopes::ReceiptScope
5
+ include OnStomp::Interfaces::FrameMethods
6
+
7
+ attr_reader :callback, :client, :connection
8
+
9
+ def initialize callback, client
10
+ @callback = callback
11
+ @client = client
12
+ @connectio = client.connection
13
+ end
14
+
15
+ # Wraps {OnStomp::Client#transmit}, applying the {#callback} as a receipt
16
+ # handler for all frames before they are sent to the broker.
17
+ # @param [OnStomp::Components::Frame] frame
18
+ # @param [{Symbol => Proc}] cbs
19
+ # @return [OnStomp::Components::Frame]
20
+ # @see OnStomp::Client#transmit
21
+ def transmit frame, cbs={}
22
+ cbs[:receipt] ||= callback
23
+ client.transmit frame, cbs
24
+ end
25
+ end
@@ -0,0 +1,191 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Bundles supported frames within a transaction. The transaction only applies
4
+ # to SEND, BEGIN, COMMIT, ABORT, ACK, and NACK frames, all others are
5
+ # passed on to the broker unmodified. A given transaction scope can be used
6
+ # to wrap multiple transactions as once {#abort} or {#commit} has been called,
7
+ # a call to {#begin} will generate a new transaction id.
8
+ class OnStomp::Components::Scopes::TransactionScope
9
+ include OnStomp::Interfaces::FrameMethods
10
+
11
+ # The id of the current transaction. This may be +nil+ if the transaction
12
+ # has not been started with {#begin} or if the transaction has been completed
13
+ # by a call to either {#abort} or {#commit}.
14
+ # @return [String,nil]
15
+ attr_reader :transaction
16
+
17
+ # The client this transaction belongs to
18
+ # @return [OnStomp::Client]
19
+ attr_reader :client
20
+
21
+ # A reference to +self+ to trick {OnStomp::Interfaces::FrameMethods} into
22
+ # creating frames on this object instead of the client's actual
23
+ # {OnStomp::Client#connection connection}.
24
+ # @return [self]
25
+ attr_reader :connection
26
+
27
+ def initialize tx_id, client
28
+ @transaction = tx_id
29
+ @client = client
30
+ @connection = self
31
+ @started = false
32
+ end
33
+
34
+ # Overrides the standard {OnStomp::Interfaces::FrameMethods#begin} method
35
+ # to maintain the state of the transaction. Unlike
36
+ # {OnStomp::Interfaces::FrameMethods#begin}, no transaction ID parameter is
37
+ # required when {#begin} is called on a
38
+ # {OnStomp::Components::Scopes::TransactionScope}. If a transaction ID is
39
+ # provided, it will be used, otherwise one will be automatically generated.
40
+ # @param [{#to_sym => #to_s}] headers optional headers to include in the frame
41
+ # @raise [OnStomp::TransactionError] if {#begin} has already been called and
42
+ # neither {#abort} or {#commit} have been called to complete the transaction.
43
+ # @return [OnStomp::Components::Frame] BEGIN frame
44
+ def begin_with_transaction *args
45
+ raise OnStomp::TransactionError, 'transaction has already begun' if @started
46
+ headers = args.last.is_a?(Hash) ? args.pop : {}
47
+ next_transaction_id args.first
48
+ @started = true
49
+ begin_without_transaction @transaction, headers
50
+ end
51
+ alias :begin_without_transaction :begin
52
+ alias :begin :begin_with_transaction
53
+
54
+ # Overrides the standard {OnStomp::Interfaces::FrameMethods#commit} method
55
+ # to maintain the state of the transaction. Unlike
56
+ # {OnStomp::Interfaces::FrameMethods#commit}, no transaction ID parameter is
57
+ # required when {#commit} is called on a
58
+ # {OnStomp::Components::Scopes::TransactionScope}. If a transaction ID is
59
+ # provided, it will be ignored.
60
+ # @param [{#to_sym => #to_s}] headers optional headers to include in the frame
61
+ # @return [OnStomp::Components::Frame] COMMIT frame
62
+ def commit_with_transaction *args
63
+ raise OnStomp::TransactionError, 'transaction has not begun' unless @started
64
+ headers = args.last.is_a?(Hash) ? args.pop : {}
65
+ commit_without_transaction(@transaction, headers).tap do
66
+ finalize_transaction
67
+ end
68
+ end
69
+ alias :commit_without_transaction :commit
70
+ alias :commit :commit_with_transaction
71
+
72
+ # Overrides the standard {OnStomp::Interfaces::FrameMethods#abort} method
73
+ # to maintain the state of the transaction. Unlike
74
+ # {OnStomp::Interfaces::FrameMethods#abort}, no transaction ID parameter is
75
+ # required when {#abort} is called on a
76
+ # {OnStomp::Components::Scopes::TransactionScope}. If a transaction ID is
77
+ # provided, it will be ignored.
78
+ # @param [{#to_sym => #to_s}] headers optional headers to include in the frame
79
+ # @return [OnStomp::Components::Frame] ABORT frame
80
+ def abort_with_transaction *args
81
+ raise OnStomp::TransactionError, 'transaction has not begun' unless @started
82
+ headers = args.last.is_a?(Hash) ? args.pop : {}
83
+ abort_without_transaction(@transaction, headers).tap do
84
+ finalize_transaction
85
+ end
86
+ end
87
+ alias :abort_without_transaction :abort
88
+ alias :abort :abort_with_transaction
89
+
90
+ # Overrides the {OnStomp::Connections::Stomp_1#send_frame send_frame} method
91
+ # of the {OnStomp::Client#connection client's connection}, setting a
92
+ # +transaction+ header to match the current transaction if it has been
93
+ # started.
94
+ # @param [arg1, arg2, ...] args arguments to connection's +send_frame+ method
95
+ # @return [OnStomp::Components::Frame] SEND frame
96
+ def send_frame *args, &blk
97
+ client.connection.send_frame(*args,&blk).tap do |f|
98
+ f[:transaction] = @transaction if @started
99
+ end
100
+ end
101
+
102
+ # Overrides the {OnStomp::Connections::Stomp_1_0#ack_frame ack_frame} method
103
+ # of the client's {OnStomp::Client#connection connection}, setting a
104
+ # +transaction+ header to match the current transaction if it has been
105
+ # started.
106
+ # @param [arg1, arg2, ...] args arguments to connection's +ack_frame+ method
107
+ # @return [OnStomp::Components::Frame] ACK frame
108
+ def ack_frame *args
109
+ client.connection.ack_frame(*args).tap do |f|
110
+ f[:transaction] = @transaction if @started
111
+ end
112
+ end
113
+
114
+ # Overrides the {OnStomp::Connections::Stomp_1_1#nack_frame nack_frame} method
115
+ # of the {OnStomp::Client#connection client's connection}, setting a
116
+ # +transaction+ header to match the current transaction if it has been
117
+ # started.
118
+ # @param [arg1, arg2, ...] args arguments to connection's +nack_frame+ method
119
+ # @return [OnStomp::Components::Frame] NACK frame
120
+ def nack_frame *args
121
+ client.connection.ack_frame(*args).tap do |f|
122
+ f[:transaction] = @transaction if @started
123
+ end
124
+ end
125
+
126
+ # If the name of the missing method ends with +_frame+, the method is passed
127
+ # along to the client's {OnStomp::Client#connection connection} so that it
128
+ # might build the appropriate (non-transactional) frame.
129
+ # @return [OnStomp::Components::Frame]
130
+ # @raise [OnStomp::UnsupportedCommandError] if the connection does not
131
+ # support the requested frame command.
132
+ # @raise [NoMethodError] if the method name does not end in +_frame+
133
+ def method_missing meth, *args, &block
134
+ if meth.to_s =~ /^(.*)_frame$/
135
+ client.connection.__send__(meth, *args, &block)
136
+ else
137
+ super
138
+ end
139
+ end
140
+
141
+ # Evaluates a block within this transaction scope. This method will transmit
142
+ # a BEGIN frame to start the transaction (unless it was manually begun prior
143
+ # to calling {#perform}), yield itself to the supplied block, and finally
144
+ # transmit a COMMIT frame to complete the transaction if no errors were
145
+ # raised within the block. If an error was raised within the block, an
146
+ # ABORT frame will be transmitted instead, rolling back the transaction and
147
+ # the exception will be re-raised.
148
+ # If a non-transactional frame is generated within the block, it will be
149
+ # transmitted as-is to the broker and will not be considered part of the
150
+ # transaction.
151
+ # Finally, if the {#abort} or {#commit} methods are called within the block,
152
+ # neither COMMIT nor ABORT frames will be automatically generated after
153
+ # the block's execution.
154
+ # @return [self]
155
+ # @raise [Exception] if supplied block raises an exception
156
+ # @yield [t] block of frames to transmit transactionally
157
+ # @yieldparam [OnStomp::Components::Scopes::TransactionScope] t +self+
158
+ def perform
159
+ begin
160
+ self.begin unless @started
161
+ yield self
162
+ self.commit if @started
163
+ self
164
+ rescue Exception
165
+ self.abort if @started
166
+ raise
167
+ end
168
+ end
169
+
170
+ # Wraps {OnStomp::Client#transmit} to support the
171
+ # {OnStomp::Interfaces::FrameMethods} mixin. All arguments are directly
172
+ # passed on to the {#client}.
173
+ # @return [OnStomp::Components::Frame]
174
+ # @see OnStomp::Client#transmit
175
+ def transmit *args
176
+ client.transmit *args
177
+ end
178
+
179
+ private
180
+ def finalize_transaction
181
+ @transaction = nil
182
+ @started = false
183
+ end
184
+
185
+ def next_transaction_id maybe_tx
186
+ # find the first non-nil, non-empty value
187
+ tx_val = [maybe_tx, @transaction].detect { |t| !t.nil? && !t.empty? }
188
+ # If both are nil or empty, generate a new serial id
189
+ @transaction = tx_val || OnStomp.next_serial
190
+ end
191
+ end
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Mixin for {OnStomp::Client clients} to create frame scopes.
4
+ module OnStomp::Components::Scopes
5
+ # Creates a new {OnStomp::Components::Scopes::ReceiptScope}.
6
+ # Any receipt-able frame generated on this scope will automatically have
7
+ # the supplied callback attached as a RECEIPT handler.
8
+ # @yield [r] callback to be invoked when the RECEIPT frame is received
9
+ # @yieldparam [OnStomp::Components::Frame] r RECEIPT frame
10
+ # @return [OnStomp::Components::Scopes::ReceiptScope]
11
+ def with_receipt &block
12
+ OnStomp::Components::Scopes::ReceiptScope.new(block, self)
13
+ end
14
+
15
+ # Creates a new {OnStomp::Components::Scopes::TransactionScope} and
16
+ # evaluates the block within that scope if one is given.
17
+ # @param [String] tx_id optional id for the transaction
18
+ # @yield [t] block of frames to generate within a transaction
19
+ # @yieldparam [OnStomp::Components::Scopes::TransactionScope] t
20
+ # @return [OnStomp::Components::Scopes::TransactionScope]
21
+ # @see OnStomp::Components::Scopes::TransactionScope#perform
22
+ def transaction tx_id=nil, &block
23
+ OnStomp::Components::Scopes::TransactionScope.new(tx_id, self).tap do |t|
24
+ t.perform(&block) if block
25
+ end
26
+ end
27
+
28
+ # Creates a new {OnStomp::Components::Scopes::HeaderScope} that
29
+ # will apply the provided headers to all frames generated on the scope.
30
+ # If a block is given, it will be evaluated within this scope.
31
+ # @param [{#to_sym => #to_s}] headers
32
+ # @yield [h] block of frames to apply headers to
33
+ # @yieldparam [OnStomp::Components::Scopes::HeaderScope] h
34
+ # @return [OnStomp::Components::Scopes::HeaderScope]
35
+ # @see OnStomp::Components::Scopes::HeaderScope#perform
36
+ def with_headers headers
37
+ OnStomp::Components::Scopes::HeaderScope.new(headers, self).tap do |h|
38
+ yield h if block_given?
39
+ end
40
+ end
41
+ end
42
+
43
+ require 'onstomp/components/scopes/header_scope'
44
+ require 'onstomp/components/scopes/receipt_scope'
45
+ require 'onstomp/components/scopes/transaction_scope'
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A simple encapsulation of a subscription. Instances of this class keep
4
+ # track of the SUBSCRIBE frame they were generated for and the callback to
5
+ # invoke when a MESSAGE frame is received for the subscription.
6
+ class OnStomp::Components::Subscription
7
+ attr_reader :frame, :callback
8
+ # Creates a new subscription
9
+ # @param [OnStomp::Components::Frame] fr the subscription's SUBSCRIBE frame
10
+ # @param [Proc] cb the subscription's callback
11
+ def initialize(fr, cb)
12
+ @frame = fr
13
+ @callback = cb
14
+ end
15
+ # Returns the +id+ header of the associated SUBSCRIBE frame
16
+ # @return [String]
17
+ def id; frame[:id]; end
18
+ # Returns the +destination+ header of the associated SUBSCRIBE frame
19
+ # @return [String]
20
+ def destination; frame[:destination]; end
21
+ # Invokes the {#callback}, passing along the supplied MESSAGE frame
22
+ # @param [OnStomp::Componenets::Frame] m the associated MESSAGE frame
23
+ def call(m); callback.call(m); end
24
+ # Returns true if this message frame shares the same destination as this
25
+ # subscription, false otherwise.
26
+ # @return [true, false]
27
+ def include? m
28
+ self.destination == m[:destination]
29
+ end
30
+ end