amqp-client 1.2.1 → 2.0.1

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/lib/amqp/client.rb CHANGED
@@ -4,6 +4,11 @@ require_relative "client/version"
4
4
  require_relative "client/connection"
5
5
  require_relative "client/exchange"
6
6
  require_relative "client/queue"
7
+ require_relative "client/consumer"
8
+ require_relative "client/rpc_client"
9
+ require_relative "client/message_codecs"
10
+ require_relative "client/message_codec_registry"
11
+ require_relative "client/configuration"
7
12
 
8
13
  # AMQP 0-9-1 Protocol, this library only implements the Client
9
14
  # @see Client
@@ -11,6 +16,11 @@ module AMQP
11
16
  # AMQP 0-9-1 Client
12
17
  # @see Connection
13
18
  class Client
19
+ # Class-level codec registry
20
+ @codec_registry = MessageCodecRegistry.new
21
+ # Class-level configuration
22
+ @config = Configuration.new(@codec_registry)
23
+
14
24
  # Create a new Client object, this won't establish a connection yet, use {#connect} or {#start} for that
15
25
  # @param uri [String] URL on the format amqp://username:password@hostname/vhost,
16
26
  # use amqps:// for encrypted connection
@@ -20,15 +30,23 @@ module AMQP
20
30
  # @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
21
31
  # @option options [Integer] frame_max (131_072) Maximum frame size,
22
32
  # the smallest of the client's and the broker's values will be used
23
- # @option options [Integer] channel_max (2048) Maxium number of channels the client will be allowed to have open.
24
- # Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
33
+ # @option options [Integer] channel_max (2048) Maximum number of channels the client will be allowed to have open.
34
+ # Maximum allowed is 65_536. The smallest of the client's and the broker's value will be used.
25
35
  def initialize(uri = "", **options)
26
36
  @uri = uri
27
37
  @options = options
28
38
  @queues = {}
29
39
  @exchanges = {}
30
- @subscriptions = Set.new
40
+ @consumers = {}
41
+ @next_consumer_id = 0
31
42
  @connq = SizedQueue.new(1)
43
+ @codec_registry = self.class.codec_registry.dup
44
+ @strict_coding = self.class.config.strict_coding
45
+ @default_content_encoding = self.class.config.default_content_encoding
46
+ @default_content_type = self.class.config.default_content_type
47
+ @start_lock = Mutex.new
48
+ @supervisor_started = false
49
+ @stopped = false
32
50
  end
33
51
 
34
52
  # @!group Connect and disconnect
@@ -39,7 +57,7 @@ module AMQP
39
57
  # @example
40
58
  # connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
41
59
  def connect(read_loop_thread: true)
42
- Connection.new(@uri, read_loop_thread: read_loop_thread, **@options)
60
+ Connection.new(@uri, read_loop_thread:, codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
43
61
  end
44
62
 
45
63
  # Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
@@ -49,38 +67,53 @@ module AMQP
49
67
  # amqp.start
50
68
  # amqp.queue("foobar")
51
69
  def start
52
- @stopped = false
53
- Thread.new(connect(read_loop_thread: false)) do |conn|
54
- Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
55
- loop do
56
- break if @stopped
57
-
58
- conn ||= connect(read_loop_thread: false)
59
- Thread.new do
60
- # restore connection in another thread, read_loop have to run
61
- conn.channel(1) # reserve channel 1 for publishes
62
- @subscriptions.each do |queue_name, no_ack, prefetch, wt, args, blk|
63
- ch = conn.channel
64
- ch.basic_qos(prefetch)
65
- ch.basic_consume(queue_name, no_ack: no_ack, worker_threads: wt, arguments: args, &blk)
70
+ return self if started?
71
+
72
+ @start_lock.synchronize do # rubocop:disable Metrics/BlockLength
73
+ return self if started?
74
+
75
+ @supervisor_started = true
76
+ @stopped = false
77
+ Thread.new(connect(read_loop_thread: false)) do |conn|
78
+ Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
79
+ loop do
80
+ break if @stopped
81
+
82
+ conn ||= connect(read_loop_thread: false)
83
+
84
+ Thread.new do
85
+ # restore connection in another thread, read_loop have to run
86
+ conn.channel(1) # reserve channel 1 for publishes
87
+ @consumers.each_value do |consumer|
88
+ ch = conn.channel
89
+ ch.basic_qos(consumer.prefetch)
90
+ consume_ok = ch.basic_consume(consumer.queue,
91
+ **consumer.basic_consume_args,
92
+ &consumer.block)
93
+ # Update the consumer with new channel and consume_ok metadata
94
+ consumer.update_consume_ok(consume_ok)
95
+ end
96
+ @connq << conn
97
+ # Remove consumers whose internal queues were already closed (e.g. cancelled during reconnect window)
98
+ @consumers.delete_if { |_, c| c.closed? }
66
99
  end
67
- @connq << conn
100
+ conn.read_loop # blocks until connection is closed, then reconnect
101
+ rescue Error => e
102
+ warn "AMQP-Client reconnect error: #{e.inspect}"
103
+ sleep @options[:reconnect_interval] || 1
104
+ ensure
105
+ @connq.clear
106
+ conn = nil
68
107
  end
69
- conn.read_loop # blocks until connection is closed, then reconnect
70
- rescue Error => e
71
- warn "AMQP-Client reconnect error: #{e.inspect}"
72
- sleep @options[:reconnect_interval] || 1
73
- ensure
74
- conn = nil
75
108
  end
76
109
  end
77
110
  self
78
111
  end
79
112
 
80
- # Close the currently open connection
113
+ # Close the currently open connection and stop the supervision / reconnection logic.
81
114
  # @return [nil]
82
115
  def stop
83
- return if @stopped
116
+ return if @stopped && !@supervisor_started
84
117
 
85
118
  @stopped = true
86
119
  return unless @connq.size.positive?
@@ -90,6 +123,12 @@ module AMQP
90
123
  nil
91
124
  end
92
125
 
126
+ # Check if the client is connected
127
+ # @return [Boolean] true if connected or currently trying to connect, false otherwise
128
+ def started?
129
+ @supervisor_started && !@stopped
130
+ end
131
+
93
132
  # @!endgroup
94
133
  # @!group High level objects
95
134
 
@@ -99,18 +138,20 @@ module AMQP
99
138
  # messages in the queue will only survive if they are published as persistent
100
139
  # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
101
140
  # (it won't be deleted until at least one consumer has consumed from it)
141
+ # @param exclusive [Boolean] If true the queue will be deleted when the connection is closed
142
+ # @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
102
143
  # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
103
144
  # @return [Queue]
104
145
  # @example
105
146
  # amqp = AMQP::Client.new.start
106
147
  # q = amqp.queue("foobar")
107
148
  # q.publish("body")
108
- def queue(name, durable: true, auto_delete: false, arguments: {})
149
+ def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
109
150
  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
110
151
 
111
152
  @queues.fetch(name) do
112
153
  with_connection do |conn|
113
- conn.channel(1).queue_declare(name, durable: durable, auto_delete: auto_delete, arguments: arguments)
154
+ conn.channel(1).queue_declare(name, durable:, auto_delete:, exclusive:, passive:, arguments:)
114
155
  end
115
156
  @queues[name] = Queue.new(self, name)
116
157
  end
@@ -126,66 +167,104 @@ module AMQP
126
167
  # @return [Exchange]
127
168
  # @example
128
169
  # amqp = AMQP::Client.new.start
129
- # x = amqp.exchange("my.hash.exchange", "x-consistent-hash")
130
- # x.publish("body", "routing-key")
131
- def exchange(name, type, durable: true, auto_delete: false, internal: false, arguments: {})
170
+ # x = amqp.exchange("my.hash.exchange", type: "x-consistent-hash")
171
+ # x.publish("body", routing_key: "routing-key")
172
+ def exchange(name, type:, durable: true, auto_delete: false, internal: false, arguments: {})
132
173
  @exchanges.fetch(name) do
133
174
  with_connection do |conn|
134
- conn.channel(1).exchange_declare(name, type, durable: durable, auto_delete: auto_delete,
135
- internal: internal, arguments: arguments)
175
+ conn.channel(1).exchange_declare(name, type:, durable:, auto_delete:, internal:, arguments:)
136
176
  end
137
177
  @exchanges[name] = Exchange.new(self, name)
138
178
  end
139
179
  end
140
180
 
141
- # Declare a fanout exchange and return a high level Exchange object
142
- # @param name [String] Name of the exchange (defaults to "amq.fanout")
143
- # @see {#exchange} for other parameters
144
- # @return [Exchange]
145
- def fanout(name = "amq.fanout", **kwargs)
146
- exchange(name, "fanout", **kwargs)
147
- end
148
-
149
181
  # Declare a direct exchange and return a high level Exchange object
150
- # @param name [String] Name of the exchange (defaults to "" for the default direct exchange)
151
- # @see {#exchange} for other parameters
182
+ # @param name [String] Name of the exchange (defaults to "amq.direct")
183
+ # @see #exchange for other parameters
152
184
  # @return [Exchange]
153
- def direct(name = "", **kwargs)
154
- return exchange(name, "direct", **kwargs) unless name.empty?
185
+ def direct_exchange(name = "amq.direct", **)
186
+ return exchange(name, type: "direct", **) unless name.empty?
155
187
 
188
+ # Return the default exchange
156
189
  @exchanges.fetch(name) do
157
190
  @exchanges[name] = Exchange.new(self, name)
158
191
  end
159
192
  end
160
193
 
194
+ # @deprecated
195
+ # @see #direct_exchange
196
+ alias direct direct_exchange
197
+
198
+ # Return a high level Exchange object for the default direct exchange
199
+ # @see #direct for parameters
200
+ # @return [Exchange]
201
+ def default_exchange(**)
202
+ direct("", **)
203
+ end
204
+
205
+ # @deprecated
206
+ # @see #default_exchange
207
+ alias default default_exchange
208
+
209
+ # Declare a fanout exchange and return a high level Exchange object
210
+ # @param name [String] Name of the exchange (defaults to "amq.fanout")
211
+ # @see #exchange for other parameters
212
+ # @return [Exchange]
213
+ def fanout_exchange(name = "amq.fanout", **)
214
+ exchange(name, type: "fanout", **)
215
+ end
216
+
217
+ # @deprecated
218
+ # @see #fanout_exchange
219
+ alias fanout fanout_exchange
220
+
161
221
  # Declare a topic exchange and return a high level Exchange object
162
222
  # @param name [String] Name of the exchange (defaults to "amq.topic")
163
- # @see {#exchange} for other parameters
223
+ # @see #exchange for other parameters
164
224
  # @return [Exchange]
165
- def topic(name = "amq.topic", **kwargs)
166
- exchange(name, "topic", **kwargs)
225
+ def topic_exchange(name = "amq.topic", **)
226
+ exchange(name, type: "topic", **)
167
227
  end
168
228
 
229
+ # @deprecated
230
+ # @see #topic_exchange
231
+ alias topic topic_exchange
232
+
169
233
  # Declare a headers exchange and return a high level Exchange object
170
234
  # @param name [String] Name of the exchange (defaults to "amq.headers")
171
- # @see {#exchange} for other parameters
235
+ # @see #exchange for other parameters
172
236
  # @return [Exchange]
173
- def headers(name = "amq.headers", **kwargs)
174
- exchange(name, "headers", **kwargs)
237
+ def headers_exchange(name = "amq.headers", **)
238
+ exchange(name, type: "headers", **)
175
239
  end
176
240
 
241
+ # @deprecated
242
+ # @see #headers_exchange
243
+ alias headers headers_exchange
244
+
177
245
  # @!endgroup
178
246
  # @!group Publish
179
247
 
180
248
  # Publish a (persistent) message and wait for confirmation
181
- # @param (see Connection::Channel#basic_publish_confirm)
249
+ # @param body [Object] The message body
250
+ # will be encoded if any matching codec is found in the client's codec registry
251
+ # @param exchange [String] Name of the exchange to publish to
252
+ # @param routing_key [String] Routing key for the message
182
253
  # @option (see Connection::Channel#basic_publish_confirm)
183
- # @return (see Connection::Channel#basic_publish_confirm)
254
+ # @return [nil]
184
255
  # @raise (see Connection::Channel#basic_publish_confirm)
185
- def publish(body, exchange, routing_key, **properties)
256
+ # @raise [Error::PublishNotConfirmed] If the message was not confirmed by the broker
257
+ # @raise [Error::UnsupportedContentType] If content type is unsupported
258
+ # @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
259
+ def publish(body, exchange:, routing_key: "", **properties)
186
260
  with_connection do |conn|
187
- properties = { delivery_mode: 2 }.merge!(properties)
188
- conn.channel(1).basic_publish_confirm(body, exchange, routing_key, **properties)
261
+ properties[:delivery_mode] ||= 2
262
+ properties = default_content_properties.merge(properties)
263
+ body = serialize_and_encode_body(body, properties)
264
+ result = conn.channel(1).basic_publish_confirm(body, exchange:, routing_key:, **properties)
265
+ raise Error::PublishNotConfirmed unless result
266
+
267
+ nil
189
268
  end
190
269
  end
191
270
 
@@ -194,10 +273,14 @@ module AMQP
194
273
  # @option (see Connection::Channel#basic_publish)
195
274
  # @return (see Connection::Channel#basic_publish)
196
275
  # @raise (see Connection::Channel#basic_publish)
197
- def publish_and_forget(body, exchange, routing_key, **properties)
276
+ # @raise [Error::UnsupportedContentType] If content type is unsupported
277
+ # @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
278
+ def publish_and_forget(body, exchange:, routing_key: "", **properties)
198
279
  with_connection do |conn|
199
- properties = { delivery_mode: 2 }.merge!(properties)
200
- conn.channel(1).basic_publish(body, exchange, routing_key, **properties)
280
+ properties[:delivery_mode] ||= 2
281
+ properties = default_content_properties.merge(properties)
282
+ body = serialize_and_encode_body(body, properties)
283
+ conn.channel(1).basic_publish(body, exchange:, routing_key:, **properties)
201
284
  end
202
285
  end
203
286
 
@@ -209,7 +292,6 @@ module AMQP
209
292
  end
210
293
  end
211
294
 
212
- # @!endgroup
213
295
  # @!group Queue actions
214
296
 
215
297
  # Consume messages from a queue
@@ -217,19 +299,41 @@ module AMQP
217
299
  # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected) (default: false)
218
300
  # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false (default: 1)
219
301
  # @param worker_threads [Integer] Number of threads processing messages (default: 1)
302
+ # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
303
+ # The proc will be called with the consumer tag as the only argument
220
304
  # @param arguments [Hash] Custom arguments to the consumer
221
305
  # @yield [Message] Delivered message from the queue
222
- # @return [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
223
- # @return [nil]
224
- def subscribe(queue, no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
306
+ # @return [Consumer] The consumer object, which can be used to cancel the consumer
307
+ def subscribe(queue, exclusive: false, no_ack: false, prefetch: 1, worker_threads: 1,
308
+ on_cancel: nil, arguments: {}, &blk)
225
309
  raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
226
310
 
227
- @subscriptions.add? [queue, no_ack, prefetch, worker_threads, arguments, blk]
228
-
229
311
  with_connection do |conn|
230
312
  ch = conn.channel
231
313
  ch.basic_qos(prefetch)
232
- ch.basic_consume(queue, no_ack: no_ack, worker_threads: worker_threads, arguments: arguments, &blk)
314
+ consumer_id = @next_consumer_id += 1
315
+ on_cancel_proc = proc do |tag|
316
+ @consumers.delete(consumer_id)
317
+ on_cancel&.call(tag)
318
+ end
319
+ basic_consume_args = { exclusive:, no_ack:, worker_threads:, on_cancel: on_cancel_proc, arguments: }
320
+ consume_ok = ch.basic_consume(queue, **basic_consume_args, &blk)
321
+ consumer = Consumer.new(client: self, channel_id: ch.id, id: consumer_id, block: blk,
322
+ queue:, consume_ok:, prefetch:, basic_consume_args:)
323
+ @consumers[consumer_id] = consumer
324
+ consumer
325
+ end
326
+ end
327
+
328
+ # Get a message from a queue
329
+ # @param queue [String] Name of the queue to get the message from
330
+ # @param no_ack [Boolean] When false the message has to be manually acknowledged (or rejected) (default: false)
331
+ # @return [Message, nil] The message from the queue or nil if the queue is empty
332
+ def get(queue, no_ack: false)
333
+ with_connection do |conn|
334
+ conn.with_channel do |ch|
335
+ ch.basic_get(queue, no_ack:)
336
+ end
233
337
  end
234
338
  end
235
339
 
@@ -239,9 +343,9 @@ module AMQP
239
343
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
240
344
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
241
345
  # @return [nil]
242
- def bind(queue, exchange, binding_key, arguments: {})
346
+ def bind(queue:, exchange:, binding_key: "", arguments: {})
243
347
  with_connection do |conn|
244
- conn.channel(1).queue_bind(queue, exchange, binding_key, arguments: arguments)
348
+ conn.channel(1).queue_bind(queue, exchange:, binding_key:, arguments:)
245
349
  end
246
350
  end
247
351
 
@@ -251,9 +355,9 @@ module AMQP
251
355
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
252
356
  # @param arguments [Hash] Arguments matching the binding that's being removed
253
357
  # @return [nil]
254
- def unbind(queue, exchange, binding_key, arguments: {})
358
+ def unbind(queue:, exchange:, binding_key: "", arguments: {})
255
359
  with_connection do |conn|
256
- conn.channel(1).queue_unbind(queue, exchange, binding_key, arguments: arguments)
360
+ conn.channel(1).queue_unbind(queue, exchange:, binding_key:, arguments:)
257
361
  end
258
362
  end
259
363
 
@@ -273,7 +377,7 @@ module AMQP
273
377
  # @return [Integer] Number of messages in the queue when deleted
274
378
  def delete_queue(name, if_unused: false, if_empty: false)
275
379
  with_connection do |conn|
276
- msgs = conn.channel(1).queue_delete(name, if_unused: if_unused, if_empty: if_empty)
380
+ msgs = conn.channel(1).queue_delete(name, if_unused:, if_empty:)
277
381
  @queues.delete(name)
278
382
  msgs
279
383
  end
@@ -283,26 +387,26 @@ module AMQP
283
387
  # @!group Exchange actions
284
388
 
285
389
  # Bind an exchange to an exchange
286
- # @param destination [String] Name of the exchange to bind
287
390
  # @param source [String] Name of the exchange to bind to
391
+ # @param destination [String] Name of the exchange to bind
288
392
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
289
393
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
290
394
  # @return [nil]
291
- def exchange_bind(destination, source, binding_key, arguments: {})
395
+ def exchange_bind(source:, destination:, binding_key: "", arguments: {})
292
396
  with_connection do |conn|
293
- conn.channel(1).exchange_bind(destination, source, binding_key, arguments: arguments)
397
+ conn.channel(1).exchange_bind(destination:, source:, binding_key:, arguments:)
294
398
  end
295
399
  end
296
400
 
297
401
  # Unbind an exchange from an exchange
298
- # @param destination [String] Name of the exchange to unbind
299
402
  # @param source [String] Name of the exchange to unbind from
403
+ # @param destination [String] Name of the exchange to unbind
300
404
  # @param binding_key [String] Binding key which the exchange is bound to the exchange with
301
405
  # @param arguments [Hash] Arguments matching the binding that's being removed
302
406
  # @return [nil]
303
- def exchange_unbind(destination, source, binding_key, arguments: {})
407
+ def exchange_unbind(source:, destination:, binding_key: "", arguments: {})
304
408
  with_connection do |conn|
305
- conn.channel(1).exchange_unbind(destination, source, binding_key, arguments: arguments)
409
+ conn.channel(1).exchange_unbind(destination:, source:, binding_key:, arguments:)
306
410
  end
307
411
  end
308
412
 
@@ -318,9 +422,122 @@ module AMQP
318
422
  end
319
423
 
320
424
  # @!endgroup
425
+ # @!group RPC
321
426
 
322
- private
427
+ # Create a RPC server for a single method/function/procedure
428
+ # @param method [String, Symbol] name of the RPC method to host (i.e. queue name on the server side)
429
+ # @param worker_threads [Integer] number of threads that process requests
430
+ # @param durable [Boolean] If true the queue will survive broker restarts
431
+ # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
432
+ # (it won't be deleted until at least one consumer has consumed from it)
433
+ # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
434
+ # @yield Block that processes the RPC request messages
435
+ # @yieldparam [String] The body of the request message
436
+ # @yieldreturn [String] The response message body
437
+ # @return (see #subscribe)
438
+ def rpc_server(method, worker_threads: 1, durable: true, auto_delete: false, arguments: {}, &_)
439
+ queue(method.to_s, durable:, auto_delete:, arguments:)
440
+ .subscribe(prefetch: worker_threads, worker_threads:) do |msg|
441
+ result = yield msg.parse
442
+ properties = { content_type: msg.properties.content_type,
443
+ content_encoding: msg.properties.content_encoding }
444
+ result_body = serialize_and_encode_body(result, properties)
445
+
446
+ msg.channel.basic_publish(result_body, exchange: "", routing_key: msg.properties.reply_to,
447
+ correlation_id: msg.properties.correlation_id, **properties)
448
+ msg.ack
449
+ rescue StandardError
450
+ msg.reject(requeue: false)
451
+ raise
452
+ end
453
+ end
323
454
 
455
+ # Do a RPC call, sends a messages, waits for a response
456
+ # @param method [String, Symbol] name of the RPC method to call (i.e. queue name on the server side)
457
+ # @param arguments [String] arguments/body to the call
458
+ # @param timeout [Numeric, nil] Number of seconds to wait for a response
459
+ # @option (see Client#publish)
460
+ # @return [String] Returns the result from the call
461
+ # @raise [Timeout::Error] if no response is received within the timeout period
462
+ def rpc_call(method, arguments, timeout: nil, **properties)
463
+ ch = with_connection(&:channel)
464
+ begin
465
+ msg = ch.basic_consume_once("amq.rabbitmq.reply-to", timeout:) do
466
+ properties = default_content_properties.merge(properties)
467
+ body = serialize_and_encode_body(arguments, properties)
468
+ ch.basic_publish(body, exchange: "", routing_key: method.to_s,
469
+ reply_to: "amq.rabbitmq.reply-to", **properties)
470
+ end
471
+ msg.parse
472
+ ensure
473
+ ch.close
474
+ end
475
+ end
476
+
477
+ # Create a reusable RPC client
478
+ # @return [RPCClient]
479
+ def rpc_client
480
+ ch = with_connection(&:channel)
481
+ RPCClient.new(ch).start
482
+ end
483
+
484
+ # @!endgroup
485
+ # @!group Message coding
486
+
487
+ class << self
488
+ # Configure the AMQP::Client class-level settings
489
+ # @yield [Configuration] Yields the configuration object for modification
490
+ # @return [Configuration] The configuration object
491
+ # @example
492
+ # AMQP::Client.configure do |config|
493
+ # config.default_content_type = "application/json"
494
+ # config.strict_coding = true
495
+ # end
496
+ def configure
497
+ yield @config if block_given?
498
+ @config
499
+ end
500
+
501
+ # Get the class-level configuration
502
+ # @return [Configuration]
503
+ attr_reader :config
504
+
505
+ # Get the class-level codec registry
506
+ # @return [MessageCodecRegistry]
507
+ attr_reader :codec_registry
508
+
509
+ # We need to set the subclass's configuration and codec registry
510
+ # because these are class instance variables, hence not inherited.
511
+ # @api private
512
+ def inherited(subclass)
513
+ super
514
+ subclass_codec_registry = @codec_registry.dup
515
+ subclass.instance_variable_set(:@codec_registry, subclass_codec_registry)
516
+ subclass.instance_variable_set(:@config, Configuration.new(subclass_codec_registry))
517
+ # Copy configuration settings from parent
518
+ subclass.config.strict_coding = @config.strict_coding
519
+ subclass.config.default_content_type = @config.default_content_type
520
+ subclass.config.default_content_encoding = @config.default_content_encoding
521
+ end
522
+ end
523
+
524
+ # Get the codec registry for this instance
525
+ # @return [MessageCodecRegistry]
526
+ attr_reader :codec_registry
527
+
528
+ # Get/set if condig should be strict, i.e. if the client should raise on unknown codecs
529
+ attr_accessor :strict_coding
530
+
531
+ # Get/set the default content_type to use when publishing messages
532
+ # @return [String, nil]
533
+ attr_accessor :default_content_type
534
+
535
+ # Get/set the default content_encoding to use when publishing messages
536
+ # @return [String, nil]
537
+ attr_accessor :default_content_encoding
538
+
539
+ # @!endgroup
540
+ #
324
541
  def with_connection
325
542
  conn = nil
326
543
  loop do
@@ -335,5 +552,53 @@ module AMQP
335
552
  @connq << conn unless conn.closed?
336
553
  end
337
554
  end
555
+
556
+ # @api private
557
+ def cancel_consumer(consumer)
558
+ @consumers.delete(consumer.id)
559
+ with_connection do |conn|
560
+ conn.channel(consumer.channel_id).basic_cancel(consumer.tag)
561
+ end
562
+ end
563
+
564
+ private
565
+
566
+ def default_content_properties
567
+ {
568
+ content_type: @default_content_type,
569
+ content_encoding: @default_content_encoding
570
+ }.compact
571
+ end
572
+
573
+ def serialize_and_encode_body(body, properties)
574
+ body = serialize_body(body, properties)
575
+ encode_body(body, properties)
576
+ end
577
+
578
+ def encode_body(body, properties)
579
+ ce = properties[:content_encoding]
580
+ coder = @codec_registry.find_coder(ce)
581
+
582
+ return coder.encode(body, properties) if coder
583
+
584
+ is_unsupported = ce && ce != ""
585
+ raise Error::UnsupportedContentEncoding, ce if is_unsupported && @strict_coding
586
+
587
+ body
588
+ end
589
+
590
+ def serialize_body(body, properties)
591
+ return body if body.is_a?(String)
592
+
593
+ ct = properties[:content_type]
594
+ parser = @codec_registry.find_parser(ct)
595
+
596
+ return parser.serialize(body, properties) if parser
597
+
598
+ is_unsupported = ct && ct != "" && ct != "text/plain"
599
+ raise Error::UnsupportedContentType, ct if is_unsupported && @strict_coding
600
+
601
+ body.to_s
602
+ end
338
603
  end
339
604
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amqp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - CloudAMQP
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
@@ -16,33 +16,22 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
- - ".github/workflows/codeql-analysis.yml"
20
- - ".github/workflows/docs.yml"
21
- - ".github/workflows/main.yml"
22
- - ".github/workflows/release.yml"
23
- - ".gitignore"
24
- - ".rubocop.yml"
25
- - ".rubocop_todo.yml"
26
- - ".yardopts"
27
- - CHANGELOG.md
28
- - CODEOWNERS
29
- - Gemfile
30
19
  - LICENSE.txt
31
- - README.md
32
- - Rakefile
33
- - amqp-client.gemspec
34
- - bin/console
35
- - bin/setup
36
20
  - lib/amqp-client.rb
37
21
  - lib/amqp/client.rb
38
22
  - lib/amqp/client/channel.rb
23
+ - lib/amqp/client/configuration.rb
39
24
  - lib/amqp/client/connection.rb
25
+ - lib/amqp/client/consumer.rb
40
26
  - lib/amqp/client/errors.rb
41
27
  - lib/amqp/client/exchange.rb
42
28
  - lib/amqp/client/frame_bytes.rb
43
29
  - lib/amqp/client/message.rb
30
+ - lib/amqp/client/message_codec_registry.rb
31
+ - lib/amqp/client/message_codecs.rb
44
32
  - lib/amqp/client/properties.rb
45
33
  - lib/amqp/client/queue.rb
34
+ - lib/amqp/client/rpc_client.rb
46
35
  - lib/amqp/client/table.rb
47
36
  - lib/amqp/client/version.rb
48
37
  homepage: https://github.com/cloudamqp/amqp-client.rb
@@ -52,6 +41,7 @@ metadata:
52
41
  homepage_uri: https://github.com/cloudamqp/amqp-client.rb
53
42
  source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
54
43
  changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
44
+ rubygems_mfa_required: 'true'
55
45
  rdoc_options: []
56
46
  require_paths:
57
47
  - lib