Class: AceMQ::AMQP::ProtobufCodec

Inherits:
Object
  • Object
show all
Defined in:
lib/acemq/amqp/codec/protobuf.rb

Overview

Reads and writes Protocol Buffers.

For high volume, and for a contract shared with services in other languages. Messages are much smaller than JSON and parse faster, at the cost of being unreadable without the schema — which is the trade, not an oversight.

There is no codec without a message type, and there cannot be one

Protobuf bytes describe nothing about themselves. A reader must already hold the type the writer used, so this codec is built with one:

codec = ProtobufCodec.new(Acme::Order)

That asymmetry with JSONCodec, which needs no argument at all, is the difference between the formats made visible rather than an awkwardness in the API. It is also why protobuf is not a name Codecs knows: a name in configuration cannot carry a class, and the Java library leaves it out of its own registry for the same reason.

A codec reads one type. Protobuf will happily parse bytes meant for one message as another where the field numbers overlap, giving a wrong answer rather than an error, so a queue carrying two types wants two consumers or a wrapper message with a oneof.

The gem

google-protobuf, required lazily and named when it is missing. The gem declares no runtime dependencies, so a service publishing JSON is not made to compile a protobuf runtime to do it.

Constant Summary collapse

CONTENT_TYPE =

What this codec writes, and what Java, Go and .NET write.

"application/x-protobuf"
ALSO_READS =

Both spellings are in use, and Go also answers for Google's own.

["application/protobuf", "application/vnd.google.protobuf"].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(message_class) ⇒ ProtobufCodec

Returns a new instance of ProtobufCodec.

Parameters:

  • message_class (Class) —

    a class generated by protoc, which is what carries the schema

Raises:

  • (DependencyMissing) —

    when google-protobuf is not installed

  • (ArgumentError) —

    when the class is not a generated message



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/acemq/amqp/codec/protobuf.rb', line 62

def initialize(message_class)
  load_runtime!
  unless message_class.is_a?(Class) && message_class < Google::Protobuf::MessageExts
    raise ArgumentError,
          "#{message_class.inspect} is not a class protoc generated. The protobuf " \
          "codec reads one message type, and takes the class generated from your .proto."
  end

  @message_class = message_class
  freeze
end

Instance Attribute Details

#message_class ⇒ Class (readonly)

The message type this codec reads and writes.

Returns:

  • (Class)


79
80
81
# File 'lib/acemq/amqp/codec/protobuf.rb', line 79

def message_class
  @message_class
end

Instance Method Details

#can_decode?(content_type) ⇒ Boolean

Accepts the two spellings of the protobuf media type, Google's own, and any +...+protobuf+ media type.

Never a message whose sender set no content type. Protobuf bytes are not recognisable and parse quietly into nonsense more often than they fail, so a codec that volunteered would report that nonsense as a success.

Returns:

  • (Boolean)


110
111
112
113
114
115
# File 'lib/acemq/amqp/codec/protobuf.rb', line 110

def can_decode?(content_type)
  lower = content_type.to_s.downcase
  return false if lower.empty?

  lower.start_with?(CONTENT_TYPE, *ALSO_READS) || lower.include?("+protobuf")
end

#content_type ⇒ Object



74
# File 'lib/acemq/amqp/codec/protobuf.rb', line 74

def content_type = CONTENT_TYPE

#decode(body) ⇒ Google::Protobuf::MessageExts

Parameters:

  • body (String)

Returns:

  • (Google::Protobuf::MessageExts)

Raises:

  • (DecodeError) —

    when the bytes are not this message type



98
99
100
101
102
# File 'lib/acemq/amqp/codec/protobuf.rb', line 98

def decode(body)
  @message_class.decode(body.to_s.b)
rescue Google::Protobuf::ParseError => e
  raise DecodeError, "this message is not a #{@message_class}: #{e.message}"
end

#encode(payload) ⇒ String

Returns the bytes to publish.

Parameters:

  • payload (Google::Protobuf::MessageExts)

Returns:

  • (String) —

    the bytes to publish

Raises:

  • (EncodeError) —

    when the payload is not this codec's message type



84
85
86
87
88
89
90
91
92
93
# File 'lib/acemq/amqp/codec/protobuf.rb', line 84

def encode(payload)
  unless payload.is_a?(@message_class)
    raise EncodeError,
          "the protobuf codec writes #{@message_class} and was given a " \
          "#{payload.class}. Publish the generated type, or use JSONCodec to have " \
          "an ordinary object encoded."
  end

  @message_class.encode(payload)
end