lib/protobuf.ex

defmodule Protobuf do
  @moduledoc """
  `protoc` should always be used to generate code instead of writing the code by hand.

  By `use` this module, macros defined in `Protobuf.DSL` will be injected. Most of thee macros
  are equal to definition in .proto files.

      defmodule Foo do
        use Protobuf, syntax: :proto3

        defstruct [:a, :b]

        field :a, 1, type: :int32
        field :b, 2, type: :string
      end

  Your Protobuf message(module) is just a normal Elixir struct. Some useful functions are also injected,
  see "Callbacks" for details. Examples:

      foo1 = Foo.new!(%{a: 1})
      foo1.b == ""
      bin = Foo.encode(foo1)
      foo1 == Foo.decode(bin)

  Except functions in "Callbacks", some other functions may be defined:

  * Extension functions when your Protobuf message use extensions. See `Protobuf.Extension` for details.
    * `put_extension(struct, extension_mod, field, value)`
    * `get_extension(struct, extension_mod, field, default \\ nil)`

  """
  defmacro __using__(opts) do
    quote location: :keep do
      import Protobuf.DSL, only: [field: 3, field: 2, oneof: 2, extend: 4, extensions: 1]
      Module.register_attribute(__MODULE__, :fields, accumulate: true)
      Module.register_attribute(__MODULE__, :oneofs, accumulate: true)
      Module.register_attribute(__MODULE__, :extends, accumulate: true)
      Module.register_attribute(__MODULE__, :extensions, [])

      @options unquote(opts)
      @before_compile Protobuf.DSL

      @behaviour Protobuf

      def new() do
        Protobuf.Builder.new(__MODULE__)
      end

      def new(attrs) do
        Protobuf.Builder.new(__MODULE__, attrs)
      end

      def new!(attrs) do
        Protobuf.Builder.new!(__MODULE__, attrs)
      end

      def transform_module() do
        nil
      end

      defoverridable transform_module: 0

      unquote(def_encode_decode())
    end
  end

  defp def_encode_decode() do
    quote do
      def decode(data), do: Protobuf.Decoder.decode(data, __MODULE__)
      def encode(struct), do: Protobuf.Encoder.encode(struct)
    end
  end

  @doc """
  Build a blank struct with default values. This and other "new" functions are
  preferred than raw building struct method like `%Foo{}`.

  In proto3, the zero values are the default values.
  """
  @callback new() :: struct

  @doc """
  Build and update the struct with passed fields.
  """
  @callback new(Enum.t()) :: struct

  @doc """
  Similar to `new/1`, but use `struct!/2` to build the struct, so
  errors will be raised if unknown keys are passed.
  """
  @callback new!(Enum.t()) :: struct

  @doc """
  Encode the struct to a protobuf binary.

  Errors may be raised if there's something wrong in the struct.
  """
  @callback encode(struct) :: binary

  @doc """
  Decode a protobuf binary to a struct.

  Errors may be raised if there's something wrong in the binary.
  """
  @callback decode(binary) :: struct

  @doc """
  Returns `nil` or a transformer module that implements the `Protobuf.TransformModule`
  behaviour.

  This function is overridable in your module.
  """
  @callback transform_module() :: module | nil

  @doc """
  It's preferable to use message's `decode` function, like:

      Foo.decode(bin)

  """
  @spec decode(binary, module) :: struct
  def decode(data, mod) do
    Protobuf.Decoder.decode(data, mod)
  end

  @doc """
  It's preferable to use message's `encode` function, like:

      Foo.encode(foo)

  """
  @spec encode(struct) :: binary
  def encode(struct) do
    Protobuf.Encoder.encode(struct)
  end

  @doc """
  Loads extensions modules.

  This function should be called in your application's `start/2` callback,
  as seen in the example below, if you wish to use extensions.

  ## Example

      def start(_type, _args) do
        Protobuf.load_extensions()
        Supervisor.start_link([], strategy: :one_for_one)
      end
  """
  @spec load_extensions() :: :ok
  def load_extensions() do
    Protobuf.Extension.__cal_extensions__()
    :ok
  end
end