lib/protox/encode.ex

defmodule Protox.Encode do
  @moduledoc """
  This module contains the functions necessary to encode protobuf messages.
  """

  import Protox.Guards
  import Bitwise

  use Protox.{
    Float,
    WireTypes
  }

  alias Protox.{
    Varint,
    Zigzag
  }

  @doc """
  Encode a protobuf message into IO data.

  ## Example
      msg = %Fiz.Foo{a: 3, b: %{1 => %Fiz.Baz{}}}
      {:ok, iodata} = Protox.Encode.encode(msg)

  """
  @deprecated "Use Protox.encode/1 instead"
  @spec encode(struct()) :: {:ok, iodata()} | {:error, any()}
  def encode(msg) do
    # A compilation error at the line below usually happen if `msg`
    # is not a struct generated by protox.
    msg.__struct__.encode(msg)
  end

  @doc """
  Throwing version of `encode/1`.
  """
  @deprecated "Use Protox.encode!/1 instead"
  @spec encode!(struct()) :: iodata() | no_return()
  def encode!(msg) do
    # A compilation error at the line below usually happen if `msg`
    # is not a struct generated by protox.
    msg.__struct__.encode!(msg)
  end

  @doc false
  @spec make_key_bytes(Protox.Types.tag(), Protox.Types.type()) :: iodata
  def make_key_bytes(tag, ty) do
    Varint.encode(make_key(tag, ty))
  end

  @doc false
  @spec make_key(Protox.Types.tag(), Protox.Types.type()) :: non_neg_integer
  def make_key(tag, ty) when is_primitive_varint(ty), do: tag <<< 3 ||| @wire_varint
  def make_key(tag, {:enum, _}), do: tag <<< 3 ||| @wire_varint
  def make_key(tag, ty) when is_primitive_fixed64(ty), do: tag <<< 3 ||| @wire_64bits
  def make_key(tag, ty) when is_delimited(ty), do: tag <<< 3 ||| @wire_delimited
  def make_key(tag, {:message, _}), do: tag <<< 3 ||| @wire_delimited
  def make_key(tag, :packed), do: tag <<< 3 ||| @wire_delimited
  def make_key(tag, :map_entry), do: tag <<< 3 ||| @wire_delimited
  def make_key(tag, ty) when is_primitive_fixed32(ty), do: tag <<< 3 ||| @wire_32bits

  @doc false
  @spec encode_varint_signed(integer) :: iodata
  def encode_varint_signed(value) do
    value |> Zigzag.encode() |> Varint.encode()
  end

  @doc false
  @spec encode_varint_64(integer) :: iodata
  def encode_varint_64(value) do
    <<res::unsigned-native-64>> = <<value::signed-native-64>>
    Varint.encode(res)
  end

  @doc false
  @spec encode_varint_32(integer) :: iodata
  def encode_varint_32(value) when value < 0 do
    encode_varint_64(value)
  end

  @doc false
  def encode_varint_32(value) do
    <<res::unsigned-native-32>> = <<value::signed-native-32>>
    Varint.encode(res)
  end

  @doc false
  @spec encode_bool(boolean) :: binary
  def encode_bool(false), do: <<0>>
  def encode_bool(true), do: <<1>>

  @doc false
  @spec encode_int32(integer) :: iodata
  def encode_int32(value), do: encode_varint_32(value)

  @doc false
  @spec encode_int64(integer) :: iodata
  def encode_int64(value), do: encode_varint_64(value)

  @doc false
  @spec encode_sint32(integer) :: iodata
  def encode_sint32(value), do: encode_varint_signed(value)

  @doc false
  @spec encode_sint64(integer) :: iodata
  def encode_sint64(value), do: encode_varint_signed(value)

  @doc false
  @spec encode_uint32(non_neg_integer) :: iodata
  def encode_uint32(value), do: encode_varint_32(value)

  @doc false
  @spec encode_uint64(non_neg_integer) :: iodata
  def encode_uint64(value), do: encode_varint_64(value)

  @doc false
  @spec encode_fixed64(integer) :: binary
  def encode_fixed64(value), do: <<value::little-64>>

  @doc false
  @spec encode_sfixed64(integer) :: binary
  def encode_sfixed64(value), do: <<value::signed-little-64>>

  @doc false
  @spec encode_fixed32(integer) :: binary
  def encode_fixed32(value), do: <<value::little-32>>

  @doc false
  @spec encode_sfixed32(integer) :: binary
  def encode_sfixed32(value), do: <<value::signed-little-32>>

  @doc false
  @spec encode_double(float | atom) :: binary
  def encode_double(:infinity), do: @positive_infinity_64
  def encode_double(:"-infinity"), do: @negative_infinity_64
  def encode_double(:nan), do: @nan_64
  def encode_double(value), do: <<value::float-little-64>>

  @doc false
  @spec encode_float(float | atom) :: binary
  def encode_float(:infinity), do: @positive_infinity_32
  def encode_float(:"-infinity"), do: @negative_infinity_32
  def encode_float(:nan), do: @nan_32
  def encode_float(value), do: <<value::float-little-32>>

  @doc false
  @spec encode_enum(integer) :: iodata
  def encode_enum(value), do: encode_varint_32(value)

  @doc false
  @spec encode_string(String.t()) :: iodata
  def encode_string(value) do
    [Varint.encode(byte_size(value)), value]
  end

  @doc false
  @spec encode_bytes(binary) :: iodata
  def encode_bytes(value) do
    [Varint.encode(byte_size(value)), value]
  end

  @doc false
  @spec encode_message(struct) :: iodata
  def encode_message(value) do
    encoded = value |> encode!() |> :binary.list_to_bin()
    [Varint.encode(byte_size(encoded)), encoded]
  end
end