amqp-client 1.2.0 → 2.0.0
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.
- checksums.yaml +4 -4
- data/lib/amqp/client/channel.rb +115 -46
- data/lib/amqp/client/configuration.rb +66 -0
- data/lib/amqp/client/connection.rb +35 -18
- data/lib/amqp/client/consumer.rb +47 -0
- data/lib/amqp/client/errors.rb +84 -9
- data/lib/amqp/client/exchange.rb +30 -28
- data/lib/amqp/client/frame_bytes.rb +3 -4
- data/lib/amqp/client/message.rb +66 -1
- data/lib/amqp/client/message_codec_registry.rb +106 -0
- data/lib/amqp/client/message_codecs.rb +43 -0
- data/lib/amqp/client/properties.rb +16 -15
- data/lib/amqp/client/queue.rb +52 -14
- data/lib/amqp/client/rpc_client.rb +56 -0
- data/lib/amqp/client/table.rb +2 -2
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +369 -61
- metadata +8 -18
- data/.github/workflows/codeql-analysis.yml +0 -41
- data/.github/workflows/docs.yml +0 -28
- data/.github/workflows/main.yml +0 -140
- data/.github/workflows/release.yml +0 -26
- data/.gitignore +0 -9
- data/.rubocop.yml +0 -30
- data/.rubocop_todo.yml +0 -65
- data/.yardopts +0 -1
- data/CHANGELOG.md +0 -109
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -18
- data/README.md +0 -193
- data/Rakefile +0 -180
- data/amqp-client.gemspec +0 -29
- data/bin/console +0 -15
- data/bin/setup +0 -8
    
        data/lib/amqp/client/errors.rb
    CHANGED
    
    | @@ -5,30 +5,37 @@ module AMQP | |
| 5 5 | 
             
                # All errors raised inherit from this class
         | 
| 6 6 | 
             
                class Error < StandardError
         | 
| 7 7 | 
             
                  # Raised when a frame that wasn't expected arrives
         | 
| 8 | 
            -
                  class  | 
| 8 | 
            +
                  class UnexpectedFrameType < Error
         | 
| 9 9 | 
             
                    def initialize(expected, actual)
         | 
| 10 | 
            -
                      super | 
| 10 | 
            +
                      super("Expected frame type '#{expected}' but got '#{actual}'")
         | 
| 11 11 | 
             
                    end
         | 
| 12 12 | 
             
                  end
         | 
| 13 13 |  | 
| 14 14 | 
             
                  # Raised when a frame doesn't end with 206
         | 
| 15 15 | 
             
                  class UnexpectedFrameEnd < Error
         | 
| 16 16 | 
             
                    def initialize(actual)
         | 
| 17 | 
            -
                      super | 
| 17 | 
            +
                      super("Expected frame end 206 but got '#{actual}'")
         | 
| 18 18 | 
             
                    end
         | 
| 19 19 | 
             
                  end
         | 
| 20 20 |  | 
| 21 21 | 
             
                  # Should never be raised as we support all official frame types
         | 
| 22 22 | 
             
                  class UnsupportedFrameType < Error
         | 
| 23 23 | 
             
                    def initialize(type)
         | 
| 24 | 
            -
                      super | 
| 24 | 
            +
                      super("Unsupported frame type '#{type}'")
         | 
| 25 25 | 
             
                    end
         | 
| 26 26 | 
             
                  end
         | 
| 27 27 |  | 
| 28 28 | 
             
                  # Raised if a frame is received but not implemented
         | 
| 29 29 | 
             
                  class UnsupportedMethodFrame < Error
         | 
| 30 30 | 
             
                    def initialize(class_id, method_id)
         | 
| 31 | 
            -
                      super | 
| 31 | 
            +
                      super("Unsupported class/method: #{class_id} #{method_id}")
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  # Raised if a message published with confirms enabled was not confirmed (nacked)
         | 
| 36 | 
            +
                  class PublishNotConfirmed < Error
         | 
| 37 | 
            +
                    def initialize
         | 
| 38 | 
            +
                      super("Message was not confirmed by the broker")
         | 
| 32 39 | 
             
                    end
         | 
| 33 40 | 
             
                  end
         | 
| 34 41 |  | 
| @@ -37,25 +44,93 @@ module AMQP | |
| 37 44 | 
             
                    def self.new(id, level, code, reason, classid = 0, methodid = 0)
         | 
| 38 45 | 
             
                      case level
         | 
| 39 46 | 
             
                      when :connection
         | 
| 40 | 
            -
                         | 
| 47 | 
            +
                        build_connection_error(code, reason, classid, methodid)
         | 
| 41 48 | 
             
                      when :channel
         | 
| 42 | 
            -
                         | 
| 49 | 
            +
                        build_channel_error(id, code, reason, classid, methodid)
         | 
| 43 50 | 
             
                      else raise ArgumentError, "invalid level '#{level}'"
         | 
| 44 51 | 
             
                      end
         | 
| 45 52 | 
             
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    private_class_method def self.build_connection_error(code, reason, classid, methodid)
         | 
| 55 | 
            +
                      klass = case code
         | 
| 56 | 
            +
                              when 320
         | 
| 57 | 
            +
                                ConnectionForced
         | 
| 58 | 
            +
                              when 501
         | 
| 59 | 
            +
                                FrameError
         | 
| 60 | 
            +
                              when 503
         | 
| 61 | 
            +
                                CommandInvalid
         | 
| 62 | 
            +
                              when 504
         | 
| 63 | 
            +
                                ChannelError
         | 
| 64 | 
            +
                              when 505
         | 
| 65 | 
            +
                                UnexpectedFrame
         | 
| 66 | 
            +
                              when 506
         | 
| 67 | 
            +
                                ResourceError
         | 
| 68 | 
            +
                              when 530
         | 
| 69 | 
            +
                                NotAllowedError
         | 
| 70 | 
            +
                              when 541
         | 
| 71 | 
            +
                                InternalError
         | 
| 72 | 
            +
                              else
         | 
| 73 | 
            +
                                ConnectionClosed
         | 
| 74 | 
            +
                              end
         | 
| 75 | 
            +
                      klass.new(code, reason, classid, methodid)
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    private_class_method def self.build_channel_error(id, code, reason, classid, methodid)
         | 
| 79 | 
            +
                      klass = case code
         | 
| 80 | 
            +
                              when 403
         | 
| 81 | 
            +
                                AccessRefused
         | 
| 82 | 
            +
                              when 404
         | 
| 83 | 
            +
                                NotFound
         | 
| 84 | 
            +
                              when 405
         | 
| 85 | 
            +
                                ResourceLocked
         | 
| 86 | 
            +
                              when 406
         | 
| 87 | 
            +
                                PreconditionFailed
         | 
| 88 | 
            +
                              else
         | 
| 89 | 
            +
                                ChannelClosed
         | 
| 90 | 
            +
                              end
         | 
| 91 | 
            +
                      klass.new(id, code, reason, classid, methodid)
         | 
| 92 | 
            +
                    end
         | 
| 46 93 | 
             
                  end
         | 
| 47 94 |  | 
| 48 95 | 
             
                  # Raised if channel is already closed
         | 
| 49 96 | 
             
                  class ChannelClosed < Error
         | 
| 50 97 | 
             
                    def initialize(id, code, reason, classid = 0, methodid = 0)
         | 
| 51 | 
            -
                      super | 
| 98 | 
            +
                      super("Channel[#{id}] closed (#{code}) #{reason} (#{classid}/#{methodid})")
         | 
| 52 99 | 
             
                    end
         | 
| 53 100 | 
             
                  end
         | 
| 54 101 |  | 
| 55 102 | 
             
                  # Raised if connection is unexpectedly closed
         | 
| 56 103 | 
             
                  class ConnectionClosed < Error
         | 
| 57 104 | 
             
                    def initialize(code, reason, classid = 0, methodid = 0)
         | 
| 58 | 
            -
                      super | 
| 105 | 
            +
                      super("Connection closed (#{code}) #{reason} (#{classid}/#{methodid})")
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  class AccessRefused < ChannelClosed; end
         | 
| 110 | 
            +
                  class NotFound < ChannelClosed; end
         | 
| 111 | 
            +
                  class ResourceLocked < ChannelClosed; end
         | 
| 112 | 
            +
                  class PreconditionFailed < ChannelClosed; end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  class ConnectionForced < ConnectionClosed; end
         | 
| 115 | 
            +
                  class FrameError < ConnectionClosed; end
         | 
| 116 | 
            +
                  class CommandInvalid < ConnectionClosed; end
         | 
| 117 | 
            +
                  class ChannelError < ConnectionClosed; end
         | 
| 118 | 
            +
                  class UnexpectedFrame < ConnectionClosed; end
         | 
| 119 | 
            +
                  class ResourceError < ConnectionClosed; end
         | 
| 120 | 
            +
                  class NotAllowedError < ConnectionClosed; end
         | 
| 121 | 
            +
                  class InternalError < ConnectionClosed; end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  # Raised if trying to parse a message with an unsupported content type
         | 
| 124 | 
            +
                  class UnsupportedContentType < Error
         | 
| 125 | 
            +
                    def initialize(content_type)
         | 
| 126 | 
            +
                      super("Unsupported content type #{content_type}")
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  # Raised if trying to parse a message with an unsupported content encoding
         | 
| 131 | 
            +
                  class UnsupportedContentEncoding < Error
         | 
| 132 | 
            +
                    def initialize(content_encoding)
         | 
| 133 | 
            +
                      super("Unsupported content encoding #{content_encoding}")
         | 
| 59 134 | 
             
                    end
         | 
| 60 135 | 
             
                  end
         | 
| 61 136 | 
             
                end
         | 
    
        data/lib/amqp/client/exchange.rb
    CHANGED
    
    | @@ -4,6 +4,8 @@ module AMQP | |
| 4 4 | 
             
              class Client
         | 
| 5 5 | 
             
                # High level representation of an exchange
         | 
| 6 6 | 
             
                class Exchange
         | 
| 7 | 
            +
                  attr_reader :name
         | 
| 8 | 
            +
             | 
| 7 9 | 
             
                  # Should only be initialized from the Client
         | 
| 8 10 | 
             
                  # @api private
         | 
| 9 11 | 
             
                  def initialize(client, name)
         | 
| @@ -11,47 +13,47 @@ module AMQP | |
| 11 13 | 
             
                    @name = name
         | 
| 12 14 | 
             
                  end
         | 
| 13 15 |  | 
| 14 | 
            -
                  # Publish to the exchange
         | 
| 15 | 
            -
                  # @param body [ | 
| 16 | 
            -
                  #  | 
| 17 | 
            -
                  # | 
| 18 | 
            -
                  # @ | 
| 19 | 
            -
                  # @ | 
| 20 | 
            -
                  # @ | 
| 21 | 
            -
                   | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
                   | 
| 25 | 
            -
             | 
| 26 | 
            -
                  #  | 
| 27 | 
            -
                  # @ | 
| 28 | 
            -
                  # @option  | 
| 29 | 
            -
                  # @ | 
| 30 | 
            -
                  # @option properties [String] user_id Can be used to verify that this is the user that published the message
         | 
| 31 | 
            -
                  # @option properties [String] app_id Can be used to indicates which app that generated the message
         | 
| 16 | 
            +
                  # Publish to the exchange, wait for confirm
         | 
| 17 | 
            +
                  # @param body [Object] The message body
         | 
| 18 | 
            +
                  #   will be encoded if any matching codec is found in the client's codec registry
         | 
| 19 | 
            +
                  # @param routing_key [String] Routing key for the message
         | 
| 20 | 
            +
                  # @option (see Client#publish)
         | 
| 21 | 
            +
                  # @raise (see Client#publish)
         | 
| 22 | 
            +
                  # @return [Exchange] self
         | 
| 23 | 
            +
                  def publish(body, routing_key: "", **properties)
         | 
| 24 | 
            +
                    @client.publish(body, exchange: @name, routing_key:, **properties)
         | 
| 25 | 
            +
                    self
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  # Publish to the exchange, without waiting for confirm
         | 
| 29 | 
            +
                  # @param (see Exchange#publish)
         | 
| 30 | 
            +
                  # @option (see Exchange#publish)
         | 
| 31 | 
            +
                  # @raise (see Exchange#publish)
         | 
| 32 32 | 
             
                  # @return [Exchange] self
         | 
| 33 | 
            -
                  def  | 
| 34 | 
            -
                    @client. | 
| 33 | 
            +
                  def publish_and_forget(body, routing_key: "", **properties)
         | 
| 34 | 
            +
                    @client.publish_and_forget(body, exchange: @name, routing_key:, **properties)
         | 
| 35 35 | 
             
                    self
         | 
| 36 36 | 
             
                  end
         | 
| 37 37 |  | 
| 38 38 | 
             
                  # Bind to another exchange
         | 
| 39 | 
            -
                  # @param  | 
| 40 | 
            -
                  # @param binding_key [String] Binding key on which messages that match might be routed ( | 
| 39 | 
            +
                  # @param source [String, Exchange] Name of the exchange to bind to, or the exchange object itself
         | 
| 40 | 
            +
                  # @param binding_key [String] Binding key on which messages that match might be routed (defaults to empty string)
         | 
| 41 41 | 
             
                  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 42 42 | 
             
                  # @return [Exchange] self
         | 
| 43 | 
            -
                  def bind( | 
| 44 | 
            -
                     | 
| 43 | 
            +
                  def bind(source, binding_key: "", arguments: {})
         | 
| 44 | 
            +
                    source = source.name unless source.is_a?(String)
         | 
| 45 | 
            +
                    @client.exchange_bind(source:, destination: @name, binding_key:, arguments:)
         | 
| 45 46 | 
             
                    self
         | 
| 46 47 | 
             
                  end
         | 
| 47 48 |  | 
| 48 49 | 
             
                  # Unbind from another exchange
         | 
| 49 | 
            -
                  # @param  | 
| 50 | 
            -
                  # @param binding_key [String] Binding key which the queue is bound to the exchange with
         | 
| 50 | 
            +
                  # @param source [String, Exchange] Name of the exchange to unbind from, or the exchange object itself
         | 
| 51 | 
            +
                  # @param binding_key [String] Binding key which the queue is bound to the exchange with (defaults to empty string)
         | 
| 51 52 | 
             
                  # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 52 53 | 
             
                  # @return [Exchange] self
         | 
| 53 | 
            -
                  def unbind( | 
| 54 | 
            -
                     | 
| 54 | 
            +
                  def unbind(source, binding_key: "", arguments: {})
         | 
| 55 | 
            +
                    source = source.name unless source.is_a?(String)
         | 
| 56 | 
            +
                    @client.exchange_unbind(source:, destination: @name, binding_key:, arguments:)
         | 
| 55 57 | 
             
                    self
         | 
| 56 58 | 
             
                  end
         | 
| 57 59 |  | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require_relative " | 
| 4 | 
            -
            require_relative " | 
| 3 | 
            +
            require_relative "properties"
         | 
| 4 | 
            +
            require_relative "table"
         | 
| 5 5 |  | 
| 6 6 | 
             
            module AMQP
         | 
| 7 7 | 
             
              class Client
         | 
| @@ -370,9 +370,8 @@ module AMQP | |
| 370 370 | 
             
                    ].pack("C S> L> a* C")
         | 
| 371 371 | 
             
                  end
         | 
| 372 372 |  | 
| 373 | 
            -
                  def self.basic_consume(id, queue, tag, no_ack, exclusive, arguments)
         | 
| 373 | 
            +
                  def self.basic_consume(id, queue, tag, no_ack, exclusive, no_wait, arguments)
         | 
| 374 374 | 
             
                    no_local = false
         | 
| 375 | 
            -
                    no_wait = false
         | 
| 376 375 | 
             
                    bits = 0
         | 
| 377 376 | 
             
                    bits |= (1 << 0) if no_local
         | 
| 378 377 | 
             
                    bits |= (1 << 1) if no_ack
         | 
    
        data/lib/amqp/client/message.rb
    CHANGED
    
    | @@ -14,8 +14,12 @@ module AMQP | |
| 14 14 | 
             
                    @redelivered = redelivered
         | 
| 15 15 | 
             
                    @properties = nil
         | 
| 16 16 | 
             
                    @body = ""
         | 
| 17 | 
            +
                    @ack_or_reject_sent = false
         | 
| 18 | 
            +
                    @parsed = nil
         | 
| 17 19 | 
             
                  end
         | 
| 18 20 |  | 
| 21 | 
            +
                  DeliveryInfo = Struct.new(:consumer_tag, :delivery_tag, :redelivered, :exchange, :routing_key, :channel)
         | 
| 22 | 
            +
             | 
| 19 23 | 
             
                  # The channel the message was deliviered to
         | 
| 20 24 | 
             
                  # @return [Connection::Channel]
         | 
| 21 25 | 
             
                  attr_reader :channel
         | 
| @@ -49,17 +53,36 @@ module AMQP | |
| 49 53 | 
             
                  # @return [String]
         | 
| 50 54 | 
             
                  attr_accessor :body
         | 
| 51 55 |  | 
| 56 | 
            +
                  def delivery_info
         | 
| 57 | 
            +
                    @delivery_info ||= DeliveryInfo.new(
         | 
| 58 | 
            +
                      consumer_tag:,
         | 
| 59 | 
            +
                      delivery_tag:,
         | 
| 60 | 
            +
                      redelivered:,
         | 
| 61 | 
            +
                      exchange:,
         | 
| 62 | 
            +
                      routing_key:,
         | 
| 63 | 
            +
                      channel:
         | 
| 64 | 
            +
                    )
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
             | 
| 52 67 | 
             
                  # Acknowledge the message
         | 
| 53 68 | 
             
                  # @return [nil]
         | 
| 54 69 | 
             
                  def ack
         | 
| 70 | 
            +
                    return if @ack_or_reject_sent
         | 
| 71 | 
            +
             | 
| 55 72 | 
             
                    @channel.basic_ack(@delivery_tag)
         | 
| 73 | 
            +
                    @ack_or_reject_sent = true
         | 
| 74 | 
            +
                    nil
         | 
| 56 75 | 
             
                  end
         | 
| 57 76 |  | 
| 58 77 | 
             
                  # Reject the message
         | 
| 59 78 | 
             
                  # @param requeue [Boolean] If true the message will be put back into the queue again, ready to be redelivered
         | 
| 60 79 | 
             
                  # @return [nil]
         | 
| 61 80 | 
             
                  def reject(requeue: false)
         | 
| 62 | 
            -
                     | 
| 81 | 
            +
                    return if @ack_or_reject_sent
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    @channel.basic_reject(@delivery_tag, requeue:)
         | 
| 84 | 
            +
                    @ack_or_reject_sent = true
         | 
| 85 | 
            +
                    nil
         | 
| 63 86 | 
             
                  end
         | 
| 64 87 |  | 
| 65 88 | 
             
                  # @see #exchange
         | 
| @@ -69,6 +92,48 @@ module AMQP | |
| 69 92 | 
             
                  def exchange_name
         | 
| 70 93 | 
             
                    @exchange
         | 
| 71 94 | 
             
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  # @!group Message coding
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  # Parse the message body based on content_type and content_encoding
         | 
| 99 | 
            +
                  # @raise [Error::UnsupportedContentEncoding] If the content encoding is not supported
         | 
| 100 | 
            +
                  # @raise [Error::UnsupportedContentType] If the content type is not supported
         | 
| 101 | 
            +
                  # @return [Object] The parsed message body
         | 
| 102 | 
            +
                  def parse
         | 
| 103 | 
            +
                    return @parsed unless @parsed.nil?
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    registry = @channel.connection.codec_registry
         | 
| 106 | 
            +
                    strict = @channel.connection.strict_coding
         | 
| 107 | 
            +
                    decoded = decode
         | 
| 108 | 
            +
                    ct = @properties&.content_type
         | 
| 109 | 
            +
                    parser = registry&.find_parser(ct)
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    return @parsed = parser.parse(decoded, @properties) if parser
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                    is_unsupported = ct && ct != "" && ct != "text/plain"
         | 
| 114 | 
            +
                    raise Error::UnsupportedContentType, ct if is_unsupported && strict
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    @parsed = decoded
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  # Decode the message body based on content_encoding
         | 
| 120 | 
            +
                  # @raise [Error::UnsupportedContentEncoding] If the content encoding is not supported
         | 
| 121 | 
            +
                  # @return [String] The decoded message body
         | 
| 122 | 
            +
                  def decode
         | 
| 123 | 
            +
                    registry = @channel.connection.codec_registry
         | 
| 124 | 
            +
                    strict = @channel.connection.strict_coding
         | 
| 125 | 
            +
                    ce = @properties&.content_encoding
         | 
| 126 | 
            +
                    coder = registry&.find_coder(ce)
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    return coder.decode(@body, @properties) if coder
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    is_unsupported = ce && ce != ""
         | 
| 131 | 
            +
                    raise Error::UnsupportedContentEncoding, ce if is_unsupported && strict
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    @body
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                  # @!endgroup
         | 
| 72 137 | 
             
                end
         | 
| 73 138 |  | 
| 74 139 | 
             
                # A published message returned by the broker due to some error
         | 
| @@ -0,0 +1,106 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module AMQP
         | 
| 4 | 
            +
              class Client
         | 
| 5 | 
            +
                # Internal registry that stores content_type parsers and content_encoding coders.
         | 
| 6 | 
            +
                # Only exact content_type and content_encoding matches are supported.
         | 
| 7 | 
            +
                class MessageCodecRegistry
         | 
| 8 | 
            +
                  def initialize
         | 
| 9 | 
            +
                    @parsers = {} # content_type => handler
         | 
| 10 | 
            +
                    @coders = {} # content_encoding => handler
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # Register a parser for a content_type
         | 
| 14 | 
            +
                  # @param content_type [String] The content_type to match
         | 
| 15 | 
            +
                  # @param parser [Object] The parser object,
         | 
| 16 | 
            +
                  #   must respond to parse(data, properties) and serialize(obj, properties)
         | 
| 17 | 
            +
                  # @return [self]
         | 
| 18 | 
            +
                  def register_parser(content_type:, parser:)
         | 
| 19 | 
            +
                    validate_parser!(parser)
         | 
| 20 | 
            +
                    @parsers[content_type] = parser
         | 
| 21 | 
            +
                    self
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  # Register a coder for a specific content_encoding
         | 
| 25 | 
            +
                  # @param content_encoding [String] The content_encoding to match
         | 
| 26 | 
            +
                  # @param coder [Object] The coder object,
         | 
| 27 | 
            +
                  #   must respond to encode(data, properties) and decode(data, properties)
         | 
| 28 | 
            +
                  # @return [self]
         | 
| 29 | 
            +
                  def register_coder(content_encoding:, coder:)
         | 
| 30 | 
            +
                    validate_coder!(coder)
         | 
| 31 | 
            +
                    @coders[content_encoding] = coder
         | 
| 32 | 
            +
                    self
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  # Find parser handler based on message properties
         | 
| 36 | 
            +
                  # @param content_type [String] The content_type to match
         | 
| 37 | 
            +
                  # @return [Object, nil] The parser object or nil if not found
         | 
| 38 | 
            +
                  def find_parser(content_type)
         | 
| 39 | 
            +
                    @parsers[content_type]
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Find coder handler based on content_encoding
         | 
| 43 | 
            +
                  # @param content_encoding [String] The content_encoding to match
         | 
| 44 | 
            +
                  # @return [Object, nil] The coder object or nil if not found
         | 
| 45 | 
            +
                  def find_coder(content_encoding)
         | 
| 46 | 
            +
                    @coders[content_encoding]
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  # Introspection helper to list all registered content types
         | 
| 50 | 
            +
                  # @return [Array<String>] List of registered content types
         | 
| 51 | 
            +
                  def list_content_types
         | 
| 52 | 
            +
                    @parsers.keys
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  # Introspection helper to list all registered content encodings
         | 
| 56 | 
            +
                  # @return [Array<String>] List of registered content encodings
         | 
| 57 | 
            +
                  def list_content_encodings
         | 
| 58 | 
            +
                    @coders.keys
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  # Enable built-in parsers for common content types
         | 
| 62 | 
            +
                  # @return [self]
         | 
| 63 | 
            +
                  def enable_builtin_parsers
         | 
| 64 | 
            +
                    register_parser(content_type: "text/plain", parser: Parsers::Plain)
         | 
| 65 | 
            +
                    register_parser(content_type: "application/json", parser: Parsers::JSONParser)
         | 
| 66 | 
            +
                    self
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  # Enable built-in coders for common content encodings
         | 
| 70 | 
            +
                  # @return [self]
         | 
| 71 | 
            +
                  def enable_builtin_coders
         | 
| 72 | 
            +
                    register_coder(content_encoding: "gzip", coder: Coders::Gzip)
         | 
| 73 | 
            +
                    register_coder(content_encoding: "deflate", coder: Coders::Deflate)
         | 
| 74 | 
            +
                    self
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  # Enable all built-in codecs (parsers and coders)
         | 
| 78 | 
            +
                  # @return [self]
         | 
| 79 | 
            +
                  def enable_builtin_codecs
         | 
| 80 | 
            +
                    enable_builtin_parsers.enable_builtin_coders
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  # Lightweight cloning: registry contents duplicated (shallow copy of handler references)
         | 
| 84 | 
            +
                  def dup
         | 
| 85 | 
            +
                    copy = self.class.new
         | 
| 86 | 
            +
                    @parsers.each { |k, v| copy.register_parser(content_type: k, parser: v) }
         | 
| 87 | 
            +
                    @coders.each { |k, v| copy.register_coder(content_encoding: k, coder: v) }
         | 
| 88 | 
            +
                    copy
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  private
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  def validate_parser!(parser)
         | 
| 94 | 
            +
                    return if parser.respond_to?(:parse) && parser.respond_to?(:serialize)
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                    raise ArgumentError, "parser must respond to parse(data, properties) and serialize(obj, properties)"
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  def validate_coder!(coder)
         | 
| 100 | 
            +
                    return if coder.respond_to?(:encode) && coder.respond_to?(:decode)
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    raise ArgumentError, "coder must respond to encode(data, properties) and decode(data, properties)"
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
              end
         | 
| 106 | 
            +
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "json"
         | 
| 4 | 
            +
            require "zlib"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module AMQP
         | 
| 7 | 
            +
              class Client
         | 
| 8 | 
            +
                module Parsers
         | 
| 9 | 
            +
                  # Plain text passthrough parser
         | 
| 10 | 
            +
                  Plain = Class.new do
         | 
| 11 | 
            +
                    def parse(data, _properties) = data
         | 
| 12 | 
            +
                    def serialize(obj, _properties) = obj.is_a?(String) ? obj : obj.to_s
         | 
| 13 | 
            +
                  end.new
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  JSONParser = Class.new do
         | 
| 16 | 
            +
                    def parse(data, _properties) = ::JSON.parse(data, symbolize_names: true)
         | 
| 17 | 
            +
                    def serialize(obj, _properties) = ::JSON.dump(obj)
         | 
| 18 | 
            +
                  end.new
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                module Coders
         | 
| 22 | 
            +
                  Gzip = Class.new do
         | 
| 23 | 
            +
                    def encode(data, _properties)
         | 
| 24 | 
            +
                      return data if data.encoding == Encoding::BINARY
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      Zlib.gzip(data)
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                    def decode(data, _properties) = Zlib.gunzip(data)
         | 
| 30 | 
            +
                  end.new
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  Deflate = Class.new do
         | 
| 33 | 
            +
                    def encode(data, _properties)
         | 
| 34 | 
            +
                      return data if data.encoding == Encoding::BINARY
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      Zlib.deflate(data)
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    def decode(data, _properties) = Zlib.inflate(data)
         | 
| 40 | 
            +
                  end.new
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require_relative " | 
| 3 | 
            +
            require_relative "table"
         | 
| 4 4 |  | 
| 5 5 | 
             
            module AMQP
         | 
| 6 6 | 
             
              class Client
         | 
| @@ -30,19 +30,19 @@ module AMQP | |
| 30 30 | 
             
                  # @return [Hash] Properties
         | 
| 31 31 | 
             
                  def to_h
         | 
| 32 32 | 
             
                    {
         | 
| 33 | 
            -
                      content_type | 
| 34 | 
            -
                      content_encoding | 
| 35 | 
            -
                      headers | 
| 36 | 
            -
                      delivery_mode | 
| 37 | 
            -
                      priority | 
| 38 | 
            -
                      correlation_id | 
| 39 | 
            -
                      reply_to | 
| 40 | 
            -
                      expiration | 
| 41 | 
            -
                      message_id | 
| 42 | 
            -
                      timestamp | 
| 43 | 
            -
                      type | 
| 44 | 
            -
                      user_id | 
| 45 | 
            -
                      app_id: | 
| 33 | 
            +
                      content_type:,
         | 
| 34 | 
            +
                      content_encoding:,
         | 
| 35 | 
            +
                      headers:,
         | 
| 36 | 
            +
                      delivery_mode:,
         | 
| 37 | 
            +
                      priority:,
         | 
| 38 | 
            +
                      correlation_id:,
         | 
| 39 | 
            +
                      reply_to:,
         | 
| 40 | 
            +
                      expiration:,
         | 
| 41 | 
            +
                      message_id:,
         | 
| 42 | 
            +
                      timestamp:,
         | 
| 43 | 
            +
                      type:,
         | 
| 44 | 
            +
                      user_id:,
         | 
| 45 | 
            +
                      app_id:
         | 
| 46 46 | 
             
                    }
         | 
| 47 47 | 
             
                  end
         | 
| 48 48 |  | 
| @@ -184,7 +184,8 @@ module AMQP | |
| 184 184 | 
             
                    end
         | 
| 185 185 |  | 
| 186 186 | 
             
                    if (timestamp = properties[:timestamp])
         | 
| 187 | 
            -
                      timestamp.is_a?(Integer) || timestamp.is_a?(Time) || | 
| 187 | 
            +
                      timestamp.is_a?(Integer) || timestamp.is_a?(Time) ||
         | 
| 188 | 
            +
                        raise(ArgumentError, "timestamp must be an Integer or a Time")
         | 
| 188 189 |  | 
| 189 190 | 
             
                      flags |= (1 << 6)
         | 
| 190 191 | 
             
                      arr << timestamp.to_i
         | 
    
        data/lib/amqp/client/queue.rb
    CHANGED
    
    | @@ -1,9 +1,13 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require_relative "consumer"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            module AMQP
         | 
| 4 6 | 
             
              class Client
         | 
| 5 7 | 
             
                # Queue abstraction
         | 
| 6 8 | 
             
                class Queue
         | 
| 9 | 
            +
                  attr_reader :name
         | 
| 10 | 
            +
             | 
| 7 11 | 
             
                  # Should only be initialized from the Client
         | 
| 8 12 | 
             
                  # @api private
         | 
| 9 13 | 
             
                  def initialize(client, name)
         | 
| @@ -12,45 +16,79 @@ module AMQP | |
| 12 16 | 
             
                  end
         | 
| 13 17 |  | 
| 14 18 | 
             
                  # Publish to the queue, wait for confirm
         | 
| 15 | 
            -
                  # @param  | 
| 19 | 
            +
                  # @param body [Object] The message body
         | 
| 20 | 
            +
                  #   will be encoded if any matching codec is found in the client's codec registry
         | 
| 16 21 | 
             
                  # @option (see Client#publish)
         | 
| 17 22 | 
             
                  # @raise (see Client#publish)
         | 
| 18 | 
            -
                  # @return [self | 
| 23 | 
            +
                  # @return [Queue] self
         | 
| 19 24 | 
             
                  def publish(body, **properties)
         | 
| 20 | 
            -
                    @client.publish(body, "", @name, **properties)
         | 
| 25 | 
            +
                    @client.publish(body, exchange: "", routing_key: @name, **properties)
         | 
| 26 | 
            +
                    self
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Publish to the queue, without waiting for confirm
         | 
| 30 | 
            +
                  # @param (see Queue#publish)
         | 
| 31 | 
            +
                  # @option (see Queue#publish)
         | 
| 32 | 
            +
                  # @raise (see Queue#publish)
         | 
| 33 | 
            +
                  # @return [Queue] self
         | 
| 34 | 
            +
                  def publish_and_forget(body, **properties)
         | 
| 35 | 
            +
                    @client.publish_and_forget(body, exchange: "", routing_key: @name, **properties)
         | 
| 21 36 | 
             
                    self
         | 
| 22 37 | 
             
                  end
         | 
| 23 38 |  | 
| 24 39 | 
             
                  # Subscribe/consume from the queue
         | 
| 25 | 
            -
                  # @param no_ack [Boolean]  | 
| 40 | 
            +
                  # @param no_ack [Boolean] If true, messages are automatically acknowledged by the server upon delivery.
         | 
| 41 | 
            +
                  #   If false, messages are acknowledged only after the block completes successfully; if the block raises
         | 
| 42 | 
            +
                  #   an exception, the message is rejected and can be optionally requeued.
         | 
| 43 | 
            +
                  #   You can of course handle the ack/reject in the block yourself. (Default: false)
         | 
| 44 | 
            +
                  # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
         | 
| 26 45 | 
             
                  # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false
         | 
| 27 46 | 
             
                  # @param worker_threads [Integer] Number of threads processing messages,
         | 
| 28 47 | 
             
                  #   0 means that the thread calling this method will be blocked
         | 
| 48 | 
            +
                  # @param requeue_on_reject [Boolean] If true, messages that are rejected due to an exception in the block
         | 
| 49 | 
            +
                  #   will be requeued. Only relevant if no_ack is false. (Default: true)
         | 
| 50 | 
            +
                  # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
         | 
| 51 | 
            +
                  #   The proc will be called with the consumer tag as the only argument
         | 
| 29 52 | 
             
                  # @param arguments [Hash] Custom arguments to the consumer
         | 
| 30 53 | 
             
                  # @yield [Message] Delivered message from the queue
         | 
| 31 | 
            -
                  # @return [ | 
| 32 | 
            -
                  def subscribe(no_ack: false, prefetch: 1, worker_threads: 1,  | 
| 33 | 
            -
             | 
| 34 | 
            -
                     | 
| 54 | 
            +
                  # @return [Consumer] The consumer object, which can be used to cancel the consumer
         | 
| 55 | 
            +
                  def subscribe(no_ack: false, exclusive: false, prefetch: 1, worker_threads: 1, requeue_on_reject: true,
         | 
| 56 | 
            +
                                on_cancel: nil, arguments: {})
         | 
| 57 | 
            +
                    @client.subscribe(@name, no_ack:, exclusive:, prefetch:, worker_threads:, on_cancel:, arguments:) do |message|
         | 
| 58 | 
            +
                      yield message
         | 
| 59 | 
            +
                      message.ack unless no_ack
         | 
| 60 | 
            +
                    rescue StandardError => e
         | 
| 61 | 
            +
                      message.reject(requeue: requeue_on_reject) unless no_ack
         | 
| 62 | 
            +
                      raise e
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  # Get a message from the queue
         | 
| 67 | 
            +
                  # @param no_ack [Boolean] When false the message has to be manually acknowledged (or rejected) (default: false)
         | 
| 68 | 
            +
                  # @return [Message, nil] The message from the queue or nil if the queue is empty
         | 
| 69 | 
            +
                  def get(no_ack: false)
         | 
| 70 | 
            +
                    @client.get(@name, no_ack:)
         | 
| 35 71 | 
             
                  end
         | 
| 36 72 |  | 
| 37 73 | 
             
                  # Bind the queue to an exchange
         | 
| 38 | 
            -
                  # @param exchange [String] Name of the exchange to bind to
         | 
| 74 | 
            +
                  # @param exchange [String, Exchange] Name of the exchange to bind to, or the exchange object itself
         | 
| 39 75 | 
             
                  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
         | 
| 40 76 | 
             
                  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 41 77 | 
             
                  # @return [self]
         | 
| 42 | 
            -
                  def bind(exchange, binding_key, arguments: {})
         | 
| 43 | 
            -
                     | 
| 78 | 
            +
                  def bind(exchange, binding_key: "", arguments: {})
         | 
| 79 | 
            +
                    exchange = exchange.name unless exchange.is_a?(String)
         | 
| 80 | 
            +
                    @client.bind(queue: @name, exchange:, binding_key:, arguments:)
         | 
| 44 81 | 
             
                    self
         | 
| 45 82 | 
             
                  end
         | 
| 46 83 |  | 
| 47 84 | 
             
                  # Unbind the queue from an exchange
         | 
| 48 | 
            -
                  # @param exchange [String] Name of the exchange to unbind from
         | 
| 85 | 
            +
                  # @param exchange [String, Exchange] Name of the exchange to unbind from, or the exchange object itself
         | 
| 49 86 | 
             
                  # @param binding_key [String] Binding key which the queue is bound to the exchange with
         | 
| 50 87 | 
             
                  # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 51 88 | 
             
                  # @return [self]
         | 
| 52 | 
            -
                  def unbind(exchange, binding_key, arguments: {})
         | 
| 53 | 
            -
                     | 
| 89 | 
            +
                  def unbind(exchange, binding_key: "", arguments: {})
         | 
| 90 | 
            +
                    exchange = exchange.name unless exchange.is_a?(String)
         | 
| 91 | 
            +
                    @client.unbind(queue: @name, exchange:, binding_key:, arguments:)
         | 
| 54 92 | 
             
                    self
         | 
| 55 93 | 
             
                  end
         | 
| 56 94 |  |