amqp-client 1.0.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,44 +5,111 @@ require_relative "./table"
5
5
  module AMQP
6
6
  class Client
7
7
  # Encode/decode AMQP Properties
8
- # @!attribute content_type
9
- # @return [String] Content type of the message body
10
- # @!attribute content_encoding
11
- # @return [String] Content encoding of the body
12
- # @!attribute headers
13
- # @return [Hash<String, Object>] Custom headers
14
- # @!attribute delivery_mode
15
- # @return [Integer] 2 for persisted message, transient messages for all other values
16
- # @!attribute priority
17
- # @return [Integer] A priority of the message (between 0 and 255)
18
- # @!attribute correlation_id
19
- # @return [Integer] A correlation id, most often used used for RPC communication
20
- # @!attribute reply_to
21
- # @return [String] Queue to reply RPC responses to
22
- # @!attribute expiration
23
- # @return [Integer, String] Number of seconds the message will stay in the queue
24
- # @!attribute message_id
25
- # @return [String]
26
- # @!attribute timestamp
27
- # @return [Date] User-definable, but often used for the time the message was originally generated
28
- # @!attribute type
29
- # @return [String] User-definable, but can can indicate what kind of message this is
30
- # @!attribute user_id
31
- # @return [String] User-definable, but can be used to verify that this is the user that published the message
32
- # @!attribute app_id
33
- # @return [String] User-definable, but often indicates which app that generated the message
34
- Properties = Struct.new(:content_type, :content_encoding, :headers, :delivery_mode, :priority, :correlation_id,
35
- :reply_to, :expiration, :message_id, :timestamp, :type, :user_id, :app_id,
36
- keyword_init: true) do
8
+ class Properties
9
+ # rubocop:disable Metrics/ParameterLists
10
+ def initialize(content_type: nil, content_encoding: nil, headers: nil, delivery_mode: nil,
11
+ priority: nil, correlation_id: nil, reply_to: nil, expiration: nil,
12
+ message_id: nil, timestamp: nil, type: nil, user_id: nil, app_id: nil)
13
+ @content_type = content_type
14
+ @content_encoding = content_encoding
15
+ @headers = headers
16
+ @delivery_mode = delivery_mode
17
+ @priority = priority
18
+ @correlation_id = correlation_id
19
+ @reply_to = reply_to
20
+ @expiration = expiration
21
+ @message_id = message_id
22
+ @timestamp = timestamp
23
+ @type = type
24
+ @user_id = user_id
25
+ @app_id = app_id
26
+ end
27
+ # rubocop:enable Metrics/ParameterLists
28
+
29
+ # Properties as a Hash
30
+ # @return [Hash] Properties
31
+ def to_h
32
+ {
33
+ content_type: content_type,
34
+ content_encoding: content_encoding,
35
+ headers: headers,
36
+ delivery_mode: delivery_mode,
37
+ priority: priority,
38
+ correlation_id: correlation_id,
39
+ reply_to: reply_to,
40
+ expiration: expiration,
41
+ message_id: message_id,
42
+ timestamp: timestamp,
43
+ type: type,
44
+ user_id: user_id,
45
+ app_id: app_id
46
+ }
47
+ end
48
+
49
+ alias to_hash to_h
50
+
51
+ # Returns true if two Property objects holds the same information
52
+ # @return [Boolean]
53
+ def ==(other)
54
+ return false unless other.is_a? self.class
55
+
56
+ instance_variables.all? { |v| instance_variable_get(v) == other.instance_variable_get(v) }
57
+ end
58
+
59
+ # Content type of the message body
60
+ # @return [String, nil]
61
+ attr_accessor :content_type
62
+ # Content encoding of the body
63
+ # @return [String, nil]
64
+ attr_accessor :content_encoding
65
+ # Headers, for applications and header exchange routing
66
+ # @return [Hash<String, Object>, nil]
67
+ attr_accessor :headers
68
+ # Message persistent level
69
+ # @note The exchange and queue have to durable as well for the message to be persistent
70
+ # @return [1] Transient message
71
+ # @return [2] Persistent message
72
+ # @return [nil] Not specified (implicitly transient)
73
+ attr_accessor :delivery_mode
74
+ # A priority of the message (between 0 and 255)
75
+ # @return [Integer, nil]
76
+ attr_accessor :priority
77
+ # Message correlation id, commonly used to correlate RPC requests and responses
78
+ # @return [String, nil]
79
+ attr_accessor :correlation_id
80
+ # Queue to reply RPC responses to
81
+ # @return [String, nil]
82
+ attr_accessor :reply_to
83
+ # Number of seconds the message will stay in the queue
84
+ # @return [String, nil]
85
+ attr_accessor :expiration
86
+ # Application message identifier
87
+ # @return [String, nil]
88
+ attr_accessor :message_id
89
+ # Message timestamp, often indicates when the message was originally generated
90
+ # @return [Date, nil]
91
+ attr_accessor :timestamp
92
+ # Message type name
93
+ # @return [String, nil]
94
+ attr_accessor :type
95
+ # The user that published the message
96
+ # @return [String, nil]
97
+ attr_accessor :user_id
98
+ # Name of application that generated the message
99
+ # @return [String, nil]
100
+ attr_accessor :app_id
101
+
37
102
  # Encode properties into a byte array
103
+ # @param properties [Hash]
38
104
  # @return [String] byte array
39
- def encode
105
+ def self.encode(properties)
106
+ return "\x00\x00" if properties.empty?
107
+
40
108
  flags = 0
41
109
  arr = [flags]
42
- fmt = StringIO.new(String.new("S>", capacity: 35))
43
- fmt.pos = 2
110
+ fmt = String.new("S>", capacity: 37)
44
111
 
45
- if content_type
112
+ if (content_type = properties[:content_type])
46
113
  content_type.is_a?(String) || raise(ArgumentError, "content_type must be a string")
47
114
 
48
115
  flags |= (1 << 15)
@@ -50,7 +117,7 @@ module AMQP
50
117
  fmt << "Ca*"
51
118
  end
52
119
 
53
- if content_encoding
120
+ if (content_encoding = properties[:content_encoding])
54
121
  content_encoding.is_a?(String) || raise(ArgumentError, "content_encoding must be a string")
55
122
 
56
123
  flags |= (1 << 14)
@@ -58,7 +125,7 @@ module AMQP
58
125
  fmt << "Ca*"
59
126
  end
60
127
 
61
- if headers
128
+ if (headers = properties[:headers])
62
129
  headers.is_a?(Hash) || raise(ArgumentError, "headers must be a hash")
63
130
 
64
131
  flags |= (1 << 13)
@@ -67,31 +134,31 @@ module AMQP
67
134
  fmt << "L>a*"
68
135
  end
69
136
 
70
- if delivery_mode
137
+ if (delivery_mode = properties[:delivery_mode])
71
138
  delivery_mode.is_a?(Integer) || raise(ArgumentError, "delivery_mode must be an int")
72
- delivery_mode.between?(0, 2) || raise(ArgumentError, "delivery_mode must be be between 0 and 2")
73
139
 
74
140
  flags |= (1 << 12)
75
141
  arr << delivery_mode
76
142
  fmt << "C"
77
143
  end
78
144
 
79
- if priority
145
+ if (priority = properties[:priority])
80
146
  priority.is_a?(Integer) || raise(ArgumentError, "priority must be an int")
147
+
81
148
  flags |= (1 << 11)
82
149
  arr << priority
83
150
  fmt << "C"
84
151
  end
85
152
 
86
- if correlation_id
87
- priority.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
153
+ if (correlation_id = properties[:correlation_id])
154
+ correlation_id.is_a?(String) || raise(ArgumentError, "correlation_id must be a string")
88
155
 
89
156
  flags |= (1 << 10)
90
157
  arr << correlation_id.bytesize << correlation_id
91
158
  fmt << "Ca*"
92
159
  end
93
160
 
94
- if reply_to
161
+ if (reply_to = properties[:reply_to])
95
162
  reply_to.is_a?(String) || raise(ArgumentError, "reply_to must be a string")
96
163
 
97
164
  flags |= (1 << 9)
@@ -99,8 +166,8 @@ module AMQP
99
166
  fmt << "Ca*"
100
167
  end
101
168
 
102
- if expiration
103
- self.expiration = expiration.to_s if expiration.is_a?(Integer)
169
+ if (expiration = properties[:expiration])
170
+ expiration = expiration.to_s if expiration.is_a?(Integer)
104
171
  expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
105
172
 
106
173
  flags |= (1 << 8)
@@ -108,7 +175,7 @@ module AMQP
108
175
  fmt << "Ca*"
109
176
  end
110
177
 
111
- if message_id
178
+ if (message_id = properties[:message_id])
112
179
  message_id.is_a?(String) || raise(ArgumentError, "message_id must be a string")
113
180
 
114
181
  flags |= (1 << 7)
@@ -116,7 +183,7 @@ module AMQP
116
183
  fmt << "Ca*"
117
184
  end
118
185
 
119
- if timestamp
186
+ if (timestamp = properties[:timestamp])
120
187
  timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
121
188
 
122
189
  flags |= (1 << 6)
@@ -124,7 +191,7 @@ module AMQP
124
191
  fmt << "Q>"
125
192
  end
126
193
 
127
- if type
194
+ if (type = properties[:type])
128
195
  type.is_a?(String) || raise(ArgumentError, "type must be a string")
129
196
 
130
197
  flags |= (1 << 5)
@@ -132,7 +199,7 @@ module AMQP
132
199
  fmt << "Ca*"
133
200
  end
134
201
 
135
- if user_id
202
+ if (user_id = properties[:user_id])
136
203
  user_id.is_a?(String) || raise(ArgumentError, "user_id must be a string")
137
204
 
138
205
  flags |= (1 << 4)
@@ -140,7 +207,7 @@ module AMQP
140
207
  fmt << "Ca*"
141
208
  end
142
209
 
143
- if app_id
210
+ if (app_id = properties[:app_id])
144
211
  app_id.is_a?(String) || raise(ArgumentError, "app_id must be a string")
145
212
 
146
213
  flags |= (1 << 3)
@@ -149,87 +216,87 @@ module AMQP
149
216
  end
150
217
 
151
218
  arr[0] = flags
152
- arr.pack(fmt.string)
219
+ arr.pack(fmt)
153
220
  end
154
221
 
155
222
  # Decode a byte array
156
223
  # @return [Properties]
157
- def self.decode(bytes)
158
- h = new
159
- flags = bytes.unpack1("S>")
160
- pos = 2
224
+ def self.decode(bytes, pos = 0)
225
+ p = new
226
+ flags = bytes.byteslice(pos, 2).unpack1("S>")
227
+ pos += 2
161
228
  if (flags & 0x8000).positive?
162
- len = bytes[pos].ord
229
+ len = bytes.getbyte(pos)
163
230
  pos += 1
164
- h[:content_type] = bytes.byteslice(pos, len).force_encoding("utf-8")
231
+ p.content_type = bytes.byteslice(pos, len).force_encoding("utf-8")
165
232
  pos += len
166
233
  end
167
234
  if (flags & 0x4000).positive?
168
- len = bytes[pos].ord
235
+ len = bytes.getbyte(pos)
169
236
  pos += 1
170
- h[:content_encoding] = bytes.byteslice(pos, len).force_encoding("utf-8")
237
+ p.content_encoding = bytes.byteslice(pos, len).force_encoding("utf-8")
171
238
  pos += len
172
239
  end
173
240
  if (flags & 0x2000).positive?
174
241
  len = bytes.byteslice(pos, 4).unpack1("L>")
175
242
  pos += 4
176
- h[:headers] = Table.decode(bytes.byteslice(pos, len))
243
+ p.headers = Table.decode(bytes.byteslice(pos, len))
177
244
  pos += len
178
245
  end
179
246
  if (flags & 0x1000).positive?
180
- h[:delivery_mode] = bytes[pos].ord
247
+ p.delivery_mode = bytes.getbyte(pos)
181
248
  pos += 1
182
249
  end
183
250
  if (flags & 0x0800).positive?
184
- h[:priority] = bytes[pos].ord
251
+ p.priority = bytes.getbyte(pos)
185
252
  pos += 1
186
253
  end
187
254
  if (flags & 0x0400).positive?
188
- len = bytes[pos].ord
255
+ len = bytes.getbyte(pos)
189
256
  pos += 1
190
- h[:correlation_id] = bytes.byteslice(pos, len).force_encoding("utf-8")
257
+ p.correlation_id = bytes.byteslice(pos, len).force_encoding("utf-8")
191
258
  pos += len
192
259
  end
193
260
  if (flags & 0x0200).positive?
194
- len = bytes[pos].ord
261
+ len = bytes.getbyte(pos)
195
262
  pos += 1
196
- h[:reply_to] = bytes.byteslice(pos, len).force_encoding("utf-8")
263
+ p.reply_to = bytes.byteslice(pos, len).force_encoding("utf-8")
197
264
  pos += len
198
265
  end
199
266
  if (flags & 0x0100).positive?
200
- len = bytes[pos].ord
267
+ len = bytes.getbyte(pos)
201
268
  pos += 1
202
- h[:expiration] = bytes.byteslice(pos, len).force_encoding("utf-8")
269
+ p.expiration = bytes.byteslice(pos, len).force_encoding("utf-8")
203
270
  pos += len
204
271
  end
205
272
  if (flags & 0x0080).positive?
206
- len = bytes[pos].ord
273
+ len = bytes.getbyte(pos)
207
274
  pos += 1
208
- h[:message_id] = bytes.byteslice(pos, len).force_encoding("utf-8")
275
+ p.message_id = bytes.byteslice(pos, len).force_encoding("utf-8")
209
276
  pos += len
210
277
  end
211
278
  if (flags & 0x0040).positive?
212
- h[:timestamp] = Time.at(bytes.byteslice(pos, 8).unpack1("Q>"))
279
+ p.timestamp = Time.at(bytes.byteslice(pos, 8).unpack1("Q>"))
213
280
  pos += 8
214
281
  end
215
282
  if (flags & 0x0020).positive?
216
- len = bytes[pos].ord
283
+ len = bytes.getbyte(pos)
217
284
  pos += 1
218
- h[:type] = bytes.byteslice(pos, len).force_encoding("utf-8")
285
+ p.type = bytes.byteslice(pos, len).force_encoding("utf-8")
219
286
  pos += len
220
287
  end
221
288
  if (flags & 0x0010).positive?
222
- len = bytes[pos].ord
289
+ len = bytes.getbyte(pos)
223
290
  pos += 1
224
- h[:user_id] = bytes.byteslice(pos, len).force_encoding("utf-8")
291
+ p.user_id = bytes.byteslice(pos, len).force_encoding("utf-8")
225
292
  pos += len
226
293
  end
227
294
  if (flags & 0x0008).positive?
228
- len = bytes[pos].ord
295
+ len = bytes.getbyte(pos)
229
296
  pos += 1
230
- h[:app_id] = bytes.byteslice(pos, len).force_encoding("utf-8")
297
+ p.app_id = bytes.byteslice(pos, len).force_encoding("utf-8")
231
298
  end
232
- h
299
+ p
233
300
  end
234
301
  end
235
302
  end
@@ -11,30 +11,24 @@ module AMQP
11
11
  @name = name
12
12
  end
13
13
 
14
- # Publish to the queue
15
- # @param body [String] The message body
16
- # @param properties [Properties]
17
- # @option properties [String] content_type Content type of the message body
18
- # @option properties [String] content_encoding Content encoding of the body
19
- # @option properties [Hash<String, Object>] headers Custom headers
20
- # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
21
- # @option properties [Integer] priority A priority of the message (between 0 and 255)
22
- # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
23
- # @option properties [String] reply_to Queue to reply RPC responses to
24
- # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
25
- # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
26
- # @option properties [Date] timestamp Often used for the time the message was originally generated
27
- # @option properties [String] type Can indicate what kind of message this is
28
- # @option properties [String] user_id Can be used to verify that this is the user that published the message
29
- # @option properties [String] app_id Can be used to indicates which app that generated the message
30
- # @return [Queue] self
14
+ # Publish to the queue, wait for confirm
15
+ # @param (see Client#publish)
16
+ # @option (see Client#publish)
17
+ # @raise (see Client#publish)
18
+ # @return [self]
31
19
  def publish(body, **properties)
32
20
  @client.publish(body, "", @name, **properties)
33
21
  self
34
22
  end
35
23
 
36
24
  # Subscribe/consume from the queue
37
- # @return [Queue] self
25
+ # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
26
+ # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false
27
+ # @param worker_threads [Integer] Number of threads processing messages,
28
+ # 0 means that the thread calling this method will be blocked
29
+ # @param arguments [Hash] Custom arguments to the consumer
30
+ # @yield [Message] Delivered message from the queue
31
+ # @return [self]
38
32
  def subscribe(no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
39
33
  @client.subscribe(@name, no_ack: no_ack, prefetch: prefetch, worker_threads: worker_threads, arguments: arguments, &blk)
40
34
  self
@@ -44,7 +38,7 @@ module AMQP
44
38
  # @param exchange [String] Name of the exchange to bind to
45
39
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
46
40
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
47
- # @return [Queue] self
41
+ # @return [self]
48
42
  def bind(exchange, binding_key, arguments: {})
49
43
  @client.bind(@name, exchange, binding_key, arguments: arguments)
50
44
  self
@@ -54,14 +48,14 @@ module AMQP
54
48
  # @param exchange [String] Name of the exchange to unbind from
55
49
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
56
50
  # @param arguments [Hash] Arguments matching the binding that's being removed
57
- # @return [Queue] self
51
+ # @return [self]
58
52
  def unbind(exchange, binding_key, arguments: {})
59
53
  @client.unbind(@name, exchange, binding_key, arguments: arguments)
60
54
  self
61
55
  end
62
56
 
63
57
  # Purge/empty the queue
64
- # @return [Queue] self
58
+ # @return [self]
65
59
  def purge
66
60
  @client.purge(@name)
67
61
  self
@@ -5,32 +5,34 @@ module AMQP
5
5
  # Encode and decode an AMQP table to/from hash, only used internally
6
6
  # @api private
7
7
  module Table
8
- module_function
9
-
10
8
  # Encodes a hash into a byte array
9
+ # @param hash [Hash]
11
10
  # @return [String] Byte array
12
- def encode(hash)
13
- tbl = StringIO.new
14
- hash.each do |k, v|
11
+ def self.encode(hash)
12
+ return "" if hash.empty?
13
+
14
+ arr = []
15
+ fmt = String.new
16
+ hash.each do |k, value|
15
17
  key = k.to_s
16
- tbl.write [key.bytesize, key].pack("Ca*")
17
- tbl.write encode_field(v)
18
+ arr.push(key.bytesize, key)
19
+ fmt << "Ca*"
20
+ encode_field(value, arr, fmt)
18
21
  end
19
- tbl.string
22
+ arr.pack(fmt)
20
23
  end
21
24
 
22
25
  # Decodes an AMQP table into a hash
23
26
  # @return [Hash<String, Object>]
24
- def decode(bytes)
27
+ def self.decode(bytes)
25
28
  hash = {}
26
29
  pos = 0
27
30
  while pos < bytes.bytesize
28
- key_len = bytes[pos].ord
31
+ key_len = bytes.getbyte(pos)
29
32
  pos += 1
30
33
  key = bytes.byteslice(pos, key_len).force_encoding("utf-8")
31
34
  pos += key_len
32
- rest = bytes.byteslice(pos, bytes.bytesize - pos)
33
- len, value = decode_field(rest)
35
+ len, value = decode_field(bytes, pos)
34
36
  pos += len + 1
35
37
  hash[key] = value
36
38
  end
@@ -38,43 +40,58 @@ module AMQP
38
40
  end
39
41
 
40
42
  # Encoding a single value in a table
43
+ # @return [nil]
41
44
  # @api private
42
- def encode_field(value)
45
+ def self.encode_field(value, arr, fmt)
43
46
  case value
44
47
  when Integer
45
48
  if value > 2**31
46
- ["l", value].pack("a q>")
49
+ arr.push("l", value)
50
+ fmt << "aq>"
47
51
  else
48
- ["I", value].pack("a l>")
52
+ arr.push("I", value)
53
+ fmt << "al>"
49
54
  end
50
55
  when Float
51
- ["d", value].pack("a G")
56
+ arr.push("d", value)
57
+ fmt << "aG"
52
58
  when String
53
- ["S", value.bytesize, value].pack("a L> a*")
59
+ arr.push("S", value.bytesize, value)
60
+ fmt << "aL>a*"
54
61
  when Time
55
- ["T", value.to_i].pack("a Q>")
62
+ arr.push("T", value.to_i)
63
+ fmt << "aQ>"
56
64
  when Array
57
- bytes = value.map { |e| encode_field(e) }.join
58
- ["A", bytes.bytesize, bytes].pack("a L> a*")
65
+ value_arr = []
66
+ value_fmt = String.new
67
+ value.each { |e| encode_field(e, value_arr, value_fmt) }
68
+ bytes = value_arr.pack(value_fmt)
69
+ arr.push("A", bytes.bytesize, bytes)
70
+ fmt << "aL>a*"
59
71
  when Hash
60
72
  bytes = Table.encode(value)
61
- ["F", bytes.bytesize, bytes].pack("a L> a*")
73
+ arr.push("F", bytes.bytesize, bytes)
74
+ fmt << "aL>a*"
62
75
  when true
63
- ["t", 1].pack("a C")
76
+ arr.push("t", 1)
77
+ fmt << "aC"
64
78
  when false
65
- ["t", 0].pack("a C")
79
+ arr.push("t", 0)
80
+ fmt << "aC"
66
81
  when nil
67
- ["V"].pack("a")
82
+ arr << "V"
83
+ fmt << "a"
68
84
  else raise ArgumentError, "unsupported table field type: #{value.class}"
69
85
  end
86
+ nil
70
87
  end
71
88
 
72
89
  # Decodes a single value
73
- # @return [Array<Integer, Object>] Bytes read and the parsed object
90
+ # @return [Array<Integer, Object>] Bytes read and the parsed value
74
91
  # @api private
75
- def decode_field(bytes)
76
- type = bytes[0]
77
- pos = 1
92
+ def self.decode_field(bytes, pos)
93
+ type = bytes[pos]
94
+ pos += 1
78
95
  case type
79
96
  when "S"
80
97
  len = bytes.byteslice(pos, 4).unpack1("L>")
@@ -86,15 +103,17 @@ module AMQP
86
103
  [4 + len, decode(bytes.byteslice(pos, len))]
87
104
  when "A"
88
105
  len = bytes.byteslice(pos, 4).unpack1("L>")
106
+ pos += 4
107
+ array_end = pos + len
89
108
  a = []
90
- while pos < len
91
- length, value = decode_field(bytes.byteslice(pos, -1))
109
+ while pos < array_end
110
+ length, value = decode_field(bytes, pos)
92
111
  pos += length + 1
93
112
  a << value
94
113
  end
95
114
  [4 + len, a]
96
115
  when "t"
97
- [1, bytes[pos].ord == 1]
116
+ [1, bytes.getbyte(pos) == 1]
98
117
  when "b"
99
118
  [1, bytes.byteslice(pos, 1).unpack1("c")]
100
119
  when "B"
@@ -114,7 +133,7 @@ module AMQP
114
133
  when "d"
115
134
  [8, bytes.byteslice(pos, 8).unpack1("G")]
116
135
  when "D"
117
- scale = bytes[pos].ord
136
+ scale = bytes.getbyte(pos)
118
137
  pos += 1
119
138
  value = bytes.byteslice(pos, 4).unpack1("L>")
120
139
  d = value / 10**scale
@@ -3,6 +3,6 @@
3
3
  module AMQP
4
4
  class Client
5
5
  # Version of the client library
6
- VERSION = "1.0.1"
6
+ VERSION = "1.1.2"
7
7
  end
8
8
  end