lib/encoding.ex

defmodule GrpcClient.Encoding do
  # This module is derived from https://github.com/NFIBrokerage/spear/blob/9177196721d943fda2ee1a70698580b036f220ee/lib/spear/grpc.ex
  # and https://github.com/NFIBrokerage/spear/blob/main/lib/spear/request.ex

  @spec to_binary_data(tuple()) :: {iodata(), pos_integer()}
  def to_binary_data(%module{} = message) do
    {:ok, encoded} = module.encode(message)
    iodata = :binary.list_to_bin(encoded)
    length = byte_size(iodata)
    compressed = 0

    {[<<compressed::unsigned-integer-8, length::unsigned-big-integer-8-unit(4)>>, iodata],
     1 + 4 + length}
  end

  # Parse DATA frame(s)
  # See https://github.com/grpc/grpc/blob/fd27fb09b028e5823f3f246e90bad2d02dfd90d3/doc/PROTOCOL-HTTP2.md
  #
  # notes:
  # - "1 byte unsigned integer" is 8 bits, hence `unsigned-integer-8`
  # - `unit(4)` captures 4 bytes for the message_length
  # - endianness does not matter when the binary in question is 1 byte
  #   hence big/little is not specified for `compressed_flag` (however the
  #   default according to the Elixir documentation is big endian).
  #     - it _does_ matter for message_length however which is 4 bytes
  #     - this is why the specification says "big endian" for the message_length
  # - the signature of this function makes it very easy to use with
  #   Stream.unfold/2
  @spec from_binary_data(binary(), module()) :: nil | {tuple(), binary()}
  def from_binary_data(
        <<0::unsigned-integer-8, message_length::unsigned-big-integer-8-unit(4),
          encoded_message::binary-size(message_length), rest::binary>>,
        module
      ) do
    decoded = Protox.decode!(encoded_message, module)
    {decoded, rest}
  end

  def from_binary_data(_empty_or_malformed, _module), do: nil
end