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.
- data/.autotest +2 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +4 -0
- data/DeveloperNarrative.md +15 -0
- data/Gemfile +4 -0
- data/LICENSE.md +221 -0
- data/README.md +73 -0
- data/Rakefile +6 -0
- data/UserNarrative.md +8 -0
- data/examples/basic.rb +40 -0
- data/examples/events.rb +72 -0
- data/lib/onstomp/client.rb +152 -0
- data/lib/onstomp/components/frame.rb +108 -0
- data/lib/onstomp/components/frame_headers.rb +212 -0
- data/lib/onstomp/components/nil_processor.rb +20 -0
- data/lib/onstomp/components/scopes/header_scope.rb +25 -0
- data/lib/onstomp/components/scopes/receipt_scope.rb +25 -0
- data/lib/onstomp/components/scopes/transaction_scope.rb +191 -0
- data/lib/onstomp/components/scopes.rb +45 -0
- data/lib/onstomp/components/subscription.rb +30 -0
- data/lib/onstomp/components/threaded_processor.rb +62 -0
- data/lib/onstomp/components/uri.rb +30 -0
- data/lib/onstomp/components.rb +13 -0
- data/lib/onstomp/connections/base.rb +208 -0
- data/lib/onstomp/connections/heartbeating.rb +82 -0
- data/lib/onstomp/connections/serializers/stomp_1.rb +166 -0
- data/lib/onstomp/connections/serializers/stomp_1_0.rb +41 -0
- data/lib/onstomp/connections/serializers/stomp_1_1.rb +134 -0
- data/lib/onstomp/connections/serializers.rb +9 -0
- data/lib/onstomp/connections/stomp_1.rb +69 -0
- data/lib/onstomp/connections/stomp_1_0.rb +28 -0
- data/lib/onstomp/connections/stomp_1_1.rb +65 -0
- data/lib/onstomp/connections.rb +119 -0
- data/lib/onstomp/interfaces/client_configurable.rb +55 -0
- data/lib/onstomp/interfaces/client_events.rb +168 -0
- data/lib/onstomp/interfaces/connection_events.rb +62 -0
- data/lib/onstomp/interfaces/event_manager.rb +69 -0
- data/lib/onstomp/interfaces/frame_methods.rb +190 -0
- data/lib/onstomp/interfaces/receipt_manager.rb +33 -0
- data/lib/onstomp/interfaces/subscription_manager.rb +48 -0
- data/lib/onstomp/interfaces/uri_configurable.rb +106 -0
- data/lib/onstomp/interfaces.rb +14 -0
- data/lib/onstomp/version.rb +13 -0
- data/lib/onstomp.rb +147 -0
- data/onstomp.gemspec +29 -0
- data/spec/onstomp/client_spec.rb +265 -0
- data/spec/onstomp/components/frame_headers_spec.rb +163 -0
- data/spec/onstomp/components/frame_spec.rb +144 -0
- data/spec/onstomp/components/nil_processor_spec.rb +32 -0
- data/spec/onstomp/components/scopes/header_scope_spec.rb +27 -0
- data/spec/onstomp/components/scopes/receipt_scope_spec.rb +33 -0
- data/spec/onstomp/components/scopes/transaction_scope_spec.rb +227 -0
- data/spec/onstomp/components/scopes_spec.rb +63 -0
- data/spec/onstomp/components/subscription_spec.rb +58 -0
- data/spec/onstomp/components/threaded_processor_spec.rb +92 -0
- data/spec/onstomp/components/uri_spec.rb +33 -0
- data/spec/onstomp/connections/base_spec.rb +349 -0
- data/spec/onstomp/connections/heartbeating_spec.rb +132 -0
- data/spec/onstomp/connections/serializers/stomp_1_0_spec.rb +50 -0
- data/spec/onstomp/connections/serializers/stomp_1_1_spec.rb +99 -0
- data/spec/onstomp/connections/serializers/stomp_1_spec.rb +104 -0
- data/spec/onstomp/connections/stomp_1_0_spec.rb +54 -0
- data/spec/onstomp/connections/stomp_1_1_spec.rb +137 -0
- data/spec/onstomp/connections/stomp_1_spec.rb +113 -0
- data/spec/onstomp/connections_spec.rb +135 -0
- data/spec/onstomp/interfaces/client_events_spec.rb +108 -0
- data/spec/onstomp/interfaces/connection_events_spec.rb +55 -0
- data/spec/onstomp/interfaces/event_manager_spec.rb +72 -0
- data/spec/onstomp/interfaces/frame_methods_spec.rb +109 -0
- data/spec/onstomp/interfaces/receipt_manager_spec.rb +53 -0
- data/spec/onstomp/interfaces/subscription_manager_spec.rb +64 -0
- data/spec/onstomp_spec.rb +15 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/custom_argument_matchers.rb +51 -0
- data/spec/support/frame_matchers.rb +88 -0
- data/spec/support/shared_frame_method_examples.rb +116 -0
- data/yard_extensions.rb +32 -0
- 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
|