lib/msgpax/ext.ex

defmodule Msgpax.Ext do
  @moduledoc """
  A struct used to represent the MessagePack [Extension
  type](https://github.com/msgpack/msgpack/blob/master/spec.md#formats-ext).

  ## Examples

  Let's say we want to be able to serialize a custom type that consists of a
  byte `data` repeated `count` times. We could represent this as a `RepByte`
  struct in Elixir:

      defmodule RepByte do
        defstruct [:data, :count]
      end

  A simple (albeit not space efficient) approach to encoding such data is simply
  a binary containing `data` for `count` times: `%RepByte{data: ?a, count: 2}`
  would be encoded as `"aa"`.

  We can now define the `Msgpax.Packer` protocol for the `RepByte` struct to
  tell `Msgpax` how to encode this struct (we'll choose `10` as an arbitrary
  integer to identify the type of this extension).

      defimpl Msgpax.Packer, for: RepByte do
        @rep_byte_ext_type 10

        def pack(%RepByte{data: byte, count: count}) do
          @rep_byte_ext_type
          |> Msgpax.Ext.new(String.duplicate(<<byte>>, count))
          |> Msgpax.Packer.pack()
        end
      end

  Now, we can pack `RepByte`s:

      iex> packed = Msgpax.pack!(%RepByte{data: ?a, count: 3})
      iex> Msgpax.unpack!(packed)
      #Msgpax.Ext<10, "aaa">

  ### Unpacking

  As seen in the example above, since the `RepByte` struct is *packed* as a
  MessagePack extension, it will be unpacked as that extension later on; what we
  may want, however, is to unpack that extension back to a `RepByte` struct.

  To do this, we can pass an `:ext` option to `Msgpax.unpack/2` (and other
  unpacking functions). This option has to be a module that implements the
  `Msgpax.Ext.Unpacker` behaviour; it will be used to unpack extensions to
  arbitrary Elixir terms.

  For our `RepByte` example, we could create an unpacker module like this:

      defmodule MyExtUnpacker do
        @behaviour Msgpax.Ext.Unpacker
        @rep_byte_ext_type 10

        @impl true
        def unpack(%Msgpax.Ext{type: @rep_byte_ext_type, data: data}) do
          <<byte, _rest::binary>> = data
          {:ok, %RepByte{data: byte, count: byte_size(data)}}
        end
      end

  With this in place, we can now unpack a packed `RepByte` back to a `RepByte`
  struct:

      iex> packed = Msgpax.pack!(%RepByte{data: ?a, count: 3})
      iex> Msgpax.unpack!(packed, ext: MyExtUnpacker)
      %RepByte{data: ?a, count: 3}

  """

  @type type :: 0..127
  @type t :: %__MODULE__{type: type, data: iodata}

  defstruct [:type, :data]

  @doc """
  Creates a new `Msgpax.Ext` struct.

  `type` must be an integer in `0..127` and it will be used as the type of the
  extension (whose meaning depends on your application). `data` must be an iodata
  containing the serialized extension (whose serialization depends on your
  application).

  ## Examples

      iex> Msgpax.Ext.new(24, "foo")
      #Msgpax.Ext<24, "foo">

      iex> Msgpax.Ext.new(25, 'bar')
      #Msgpax.Ext<25, 'bar'>

  """
  def new(type, data)
      when type in 0..127 and (is_binary(data) or is_list(data)) do
    %__MODULE__{type: type, data: data}
  end

  defimpl Inspect do
    import Inspect.Algebra

    def inspect(%{type: type, data: data}, opts) do
      concat([
        "#Msgpax.Ext<",
        Inspect.Integer.inspect(type, opts),
        ", ",
        to_doc(data, opts),
        ">"
      ])
    end
  end
end