defmodule Msgpax do
@moduledoc ~S"""
This module provides functions for serializing and deserializing Elixir terms
using the [MessagePack](http://msgpack.org/) format.
## Data conversion
The following table shows how Elixir types are serialized to MessagePack types
and how MessagePack types are deserialized back to Elixir types.
Elixir | MessagePack | Elixir
--------------------------------- | ------------- | -------------
`nil` | nil | `nil`
`true` | boolean | `true`
`false` | boolean | `false`
`-1` | integer | `-1`
`1.25` | float | `1.25`
*N/A*¹ | NaN | `Msgpax.NaN`²
*N/A*¹ | +infinity | `Msgpax.Infinity`²
*N/A*¹ | -infinity | `Msgpax.NegInfinity`²
`:ok` | string | `"ok"`
`Atom` | string | `"Elixir.Atom"`
`"text"` | string | `"text"`
`"\xFF\xFF"` | string | `"\xFF\xFF"`
`#Msgpax.Bin<"\xFF">` | binary | `"\xFF"`³
`%{foo: "bar"}` | map | `%{"foo" => "bar"}`
`[foo: "bar"]` | map | `%{"foo" => "bar"}`
`[1, true]` | array | `[1, true]`
`#Msgpax.Ext<4, "02:12">` | extension | `#Msgpax.Ext<4, "02:12">`
`#DateTime<2017-12-06 00:00:00Z>` | extension | `#DateTime<2017-12-06 00:00:00Z>`
¹ `Msgpax.Packer` provides helper functions to facilitate the serialization of natively unsupported data types.
² NaN and ±infinity are not enabled by default. See `unpack/2` for for more information.
³ To deserialize back to `Msgpax.Bin` structs see the `unpack/2` options.
"""
alias __MODULE__.Packer
alias __MODULE__.Unpacker
@doc """
Serializes the given `term`.
This function returns iodata by default; if you want to force the result to be
a binary, you can use `IO.iodata_to_binary/1` or use the `:iodata` option (see
the "Options" section below).
This function returns `{:ok, iodata}` if the serialization is successful,
`{:error, exception}` otherwise, where `exception` is a `Msgpax.PackError`
struct which can be raised or converted to a more human-friendly error
message with `Exception.message/1`. See `Msgpax.PackError` for all the
possible reasons for a packing error.
## Options
* `:iodata` - (boolean) if `true`, this function returns the encoded term as
iodata, if `false` as a binary. Defaults to `true`.
## Examples
iex> {:ok, packed} = Msgpax.pack("foo")
iex> IO.iodata_to_binary(packed)
<<163, 102, 111, 111>>
iex> Msgpax.pack(20000000000000000000)
{:error, %Msgpax.PackError{reason: {:too_big, 20000000000000000000}}}
iex> Msgpax.pack("foo", iodata: false)
{:ok, <<163, 102, 111, 111>>}
"""
@spec pack(term, Keyword.t()) :: {:ok, iodata} | {:error, Msgpax.PackError.t() | Exception.t()}
def pack(term, options \\ []) when is_list(options) do
iodata? = Keyword.get(options, :iodata, true)
try do
Packer.pack(term)
catch
:throw, reason ->
{:error, %Msgpax.PackError{reason: reason}}
:error, %Protocol.UndefinedError{protocol: Msgpax.Packer} = exception ->
{:error, exception}
else
iodata when iodata? ->
{:ok, iodata}
iodata ->
{:ok, IO.iodata_to_binary(iodata)}
end
end
@doc """
Works as `pack/1`, but raises if there's an error.
This function works like `pack/1`, except it returns the `term` (instead of
`{:ok, term}`) if the serialization is successful or raises a
`Msgpax.PackError` exception otherwise.
## Options
This function accepts the same options as `pack/2`.
## Examples
iex> "foo" |> Msgpax.pack!() |> IO.iodata_to_binary()
<<163, 102, 111, 111>>
iex> Msgpax.pack!(20000000000000000000)
** (Msgpax.PackError) too big value: 20000000000000000000
iex> Msgpax.pack!("foo", iodata: false)
<<163, 102, 111, 111>>
"""
@spec pack!(term, Keyword.t()) :: iodata | no_return
def pack!(term, options \\ []) do
case pack(term, options) do
{:ok, result} ->
result
{:error, exception} ->
raise exception
end
end
@doc """
Serializes `term` and turns the result into a `Msgpax.Fragment`.
This function returns `{:ok, fragment}` if the serialization is successful,
`{:error, exception}` otherwise, where `exception` is a `Msgpax.PackError`
struct which can be raised or converted to a more human-friendly error
message with `Exception.message/1`. See `Msgpax.PackError` for all the
possible reasons for a packing error.
This is useful for optimization, for instance, to avoid packing heavy terms
repetitively.
Another good use case for fragments would be data encapsulation. For example,
if we want to "interpolate" some MessagePack data into the payload without
having to unpack and pack them.
## Options
This function accepts the same options as `pack/2`.
## Examples
iex> {:ok, fragment} = Msgpax.pack_fragment("HUGE")
iex> {:ok, packed} = Msgpax.pack([fragment, fragment])
iex> IO.iodata_to_binary(packed)
<<146, 164, 72, 85, 71, 69, 164, 72, 85, 71, 69>>
"""
@spec pack_fragment(term, Keyword.t()) ::
{:ok, Msgpax.Fragment.t()} | {:error, Msgpax.PackError.t() | Exception.t()}
def pack_fragment(term, options \\ []) do
with {:ok, packed} <- Msgpax.pack(term, options) do
{:ok, Msgpax.Fragment.new(packed)}
end
end
@doc """
Works as `pack_fragment/2`, but raises if there's an error.
This function works like `pack_fragment!/2`, except it returns the `fragment` (instead of
`{:ok, fragment}`) if the serialization is successful or raises a `Msgpax.PackError`
exception otherwise.
## Options
This function accepts the same options as `pack_fragment/2`.
## Examples
iex> fragment = Msgpax.pack_fragment!("HUGE")
iex> {:ok, packed} = Msgpax.pack([fragment, fragment])
iex> IO.iodata_to_binary(packed)
<<146, 164, 72, 85, 71, 69, 164, 72, 85, 71, 69>>
"""
@spec pack_fragment!(term, Keyword.t()) :: Msgpax.Fragment.t() | no_return()
def pack_fragment!(term, options \\ []) do
case pack_fragment(term, options) do
{:ok, fragment} -> fragment
{:error, exception} -> raise exception
end
end
@doc """
Deserializes part of the given `iodata`.
This function works like `unpack/2`, but instead of requiring the input to be
a MessagePack-serialized term with nothing after that, it accepts leftover
bytes at the end of `iodata` and only deserializes the part of the input that
makes sense. It returns `{:ok, term, rest}` if deserialization is successful,
`{:error, exception}` otherwise (where `exception` is a `Msgpax.UnpackError`
struct).
See `unpack/2` for more information on the supported options.
## Examples
iex> Msgpax.unpack_slice(<<163, "foo", "junk">>)
{:ok, "foo", "junk"}
iex> Msgpax.unpack_slice(<<163, "fo">>)
{:error, %Msgpax.UnpackError{reason: {:invalid_format, 163}}}
"""
@spec unpack_slice(iodata, Keyword.t()) :: {:ok, any, binary} | {:error, Msgpax.UnpackError.t()}
def unpack_slice(iodata, options \\ []) when is_list(options) do
try do
iodata
|> IO.iodata_to_binary()
|> Unpacker.unpack(options)
catch
:throw, reason ->
{:error, %Msgpax.UnpackError{reason: reason}}
else
{value, rest} ->
{:ok, value, rest}
end
end
@doc """
Works like `unpack_slice/2` but raises in case of error.
This function works like `unpack_slice/2`, but returns just `{term, rest}` if
deserialization is successful and raises a `Msgpax.UnpackError` exception if
it's not.
## Examples
iex> Msgpax.unpack_slice!(<<163, "foo", "junk">>)
{"foo", "junk"}
iex> Msgpax.unpack_slice!(<<163, "fo">>)
** (Msgpax.UnpackError) invalid format, first byte: 163
"""
@spec unpack_slice!(iodata, Keyword.t()) :: {any, binary} | no_return
def unpack_slice!(iodata, options \\ []) do
case unpack_slice(iodata, options) do
{:ok, value, rest} ->
{value, rest}
{:error, exception} ->
raise exception
end
end
@doc """
Deserializes the given `iodata`.
This function deserializes the given `iodata` into an Elixir term. It returns
`{:ok, term}` if the deserialization is successful, `{:error, exception}`
otherwise, where `exception` is a `Msgpax.UnpackError` struct which can be
raised or converted to a more human-friendly error message with
`Exception.message/1`. See `Msgpax.UnpackError` for all the possible reasons
for an unpacking error.
## Options
* `:binary` - (boolean) if `true`, then binaries are decoded as `Msgpax.Bin`
structs instead of plain Elixir binaries. Defaults to `false`.
* `:ext` - (module) a module that implements the `Msgpax.Ext.Unpacker`
behaviour. For more information, see the docs for `Msgpax.Ext.Unpacker`.
* `:nonfinite_floats` - (boolean) if `true`, deserializes NaN and ±infinity to
"signalling" atoms (see the "Data conversion" section), otherwise errors.
Defaults to `false`.
## Examples
iex> Msgpax.unpack(<<163, "foo">>)
{:ok, "foo"}
iex> Msgpax.unpack(<<163, "foo", "junk">>)
{:error, %Msgpax.UnpackError{reason: {:excess_bytes, "junk"}}}
iex> packed = Msgpax.pack!(Msgpax.Bin.new(<<3, 18, 122, 27, 115>>))
iex> {:ok, bin} = Msgpax.unpack(packed, binary: true)
iex> bin
#Msgpax.Bin<<<3, 18, 122, 27, 115>>>
"""
@spec unpack(iodata, Keyword.t()) :: {:ok, any} | {:error, Msgpax.UnpackError.t()}
def unpack(iodata, options \\ []) do
case unpack_slice(iodata, options) do
{:ok, value, <<>>} ->
{:ok, value}
{:ok, _, bytes} ->
{:error, %Msgpax.UnpackError{reason: {:excess_bytes, bytes}}}
{:error, _} = error ->
error
end
end
@doc """
Works like `unpack/2`, but raises in case of errors.
This function works like `unpack/2`, but it returns `term` (instead of `{:ok,
term}`) if deserialization is successful, otherwise raises a
`Msgpax.UnpackError` exception.
## Example
iex> Msgpax.unpack!(<<163, "foo">>)
"foo"
iex> Msgpax.unpack!(<<163, "foo", "junk">>)
** (Msgpax.UnpackError) found excess bytes: "junk"
iex> packed = Msgpax.pack!(Msgpax.Bin.new(<<3, 18, 122, 27, 115>>))
iex> Msgpax.unpack!(packed, binary: true)
#Msgpax.Bin<<<3, 18, 122, 27, 115>>>
"""
@spec unpack!(iodata, Keyword.t()) :: any | no_return
def unpack!(iodata, options \\ []) do
case unpack(iodata, options) do
{:ok, value} ->
value
{:error, exception} ->
raise exception
end
end
end