Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options argument to Msgpax.Packer protocol #67

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Next release
* Upgraded `Msgpax.Packer` protocol so that the pack function can receive options.

__Breaking changes:__

* `Msgpax.Packer.pack/1` changed to `Msgpax.Packer.pack/2`, so all protocol
implementations should be updated. See `Msgpax.defimpl/3` for examples.

## v2.4.0 – 2023-05-27

* Dropped support for Elixir versions before 1.6.
Expand Down
77 changes: 75 additions & 2 deletions lib/msgpax.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ defmodule Msgpax do
* `:iodata` - (boolean) if `true`, this function returns the encoded term as
iodata, if `false` as a binary. Defaults to `true`.

Any other options are passed to `Msgpax.Packer.pack/2`.
## Examples

iex> {:ok, packed} = Msgpax.pack("foo")
Expand All @@ -72,10 +73,10 @@ defmodule Msgpax do
"""
@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)
{iodata?, remaining_options} = Keyword.pop(options, :iodata, true)

try do
Packer.pack(term)
Packer.pack(term, remaining_options)
catch
:throw, reason ->
{:error, %Msgpax.PackError{reason: reason}}
Expand Down Expand Up @@ -331,4 +332,76 @@ defmodule Msgpax do
raise exception
end
end

@doc """
Works similarly to `Kernel.defimpl`, but introduces a default implementation
of the `pack/2` function that delegates the call to `pack/1`.

This macro is specifically designed for projects upgrading from versions of
`Msgpax` where the protocol required the implementation of `pack/1`.

> #### `use Msgpax` {: .info}
>
> When you `use Msgpax`, `Kernel.defimpl/2` and `Kernel.defimpl/3` are
> replaced by their `Msgpax` counterparts.

## Example
Suppose you had a custom type that implements the `Msgpax.Packer.pack/1` version:

defmodule MyCustomType do
defstruct [:foo, :bar]

defimpl Msgpax.Packer do
def pack(%MyStructure{foo: foo}), do: []
end
end

You can migrate to the new protocol by simply adding `use Msgpax`:

defmodule MyCustomType do
use Msgpax
defstruct [:foo, :bar]

defimpl Msgpax.Packer do
def pack(%MyStructure{foo: foo}), do: []
end
end

"""
defmacro defimpl(name, opts, do_block \\ []) do
protocol = Macro.expand(name, __CALLER__)

if protocol != Msgpax.Packer do
arity = (do_block == [] && "2") || "3"

raise "`Msgpax.defimpl/#{arity}` is not supported for protocols other than `Msgpax.Packer`: got `#{Macro.inspect_atom(:literal, protocol)}`"
end

for_module = Keyword.get(opts, :for, __CALLER__.module)
do_block = Keyword.get(opts, :do, do_block)

catch_all_pack_2 =
case Macro.path(do_block, &match?({:def, _, [{:pack, _, [_arg1]} | _]}, &1)) do
nil ->
[]

_ ->
quote do: def(pack(term, _options), do: @for.pack(term))
end

quote do
Kernel.defimpl Msgpax.Packer, for: unquote(for_module) do
unquote(do_block)
unquote(catch_all_pack_2)
end
end
end

@doc false
defmacro __using__(_opts) do
quote do
import Kernel, except: [defimpl: 2, defimpl: 3]
import unquote(__MODULE__), only: [defimpl: 2, defimpl: 3]
end
end
end
2 changes: 1 addition & 1 deletion lib/msgpax/fragment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule Msgpax.Fragment do
end

defimpl Msgpax.Packer do
def pack(%{data: data}), do: data
def pack(%{data: data}, _options), do: data
end

defimpl Inspect do
Expand Down
52 changes: 26 additions & 26 deletions lib/msgpax/packer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ defprotocol Msgpax.Packer do

It returns an iodata result.
"""
def pack(term)
def pack(term, options)

@doc """
Returns serialized NaN in 64-bit format.
Expand All @@ -124,23 +124,23 @@ defprotocol Msgpax.Packer do
end

defimpl Msgpax.Packer, for: Atom do
def pack(nil), do: [0xC0]
def pack(false), do: [0xC2]
def pack(true), do: [0xC3]
def pack(nil, _options), do: [0xC0]
def pack(false, _options), do: [0xC2]
def pack(true, _options), do: [0xC3]

def pack(atom) do
def pack(atom, opts) do
atom
|> Atom.to_string()
|> @protocol.BitString.pack()
|> @protocol.BitString.pack(opts)
end
end

defimpl Msgpax.Packer, for: BitString do
def pack(binary) when is_binary(binary) do
def pack(binary, _options) when is_binary(binary) do
[format(binary) | binary]
end

def pack(bits) do
def pack(bits, _options) do
throw({:not_encodable, bits})
end

Expand All @@ -162,15 +162,15 @@ defimpl Msgpax.Packer, for: Map do
@protocol.Any.deriving(module, struct, options)
end

def pack(map) do
[format(map) | map |> Map.to_list() |> pack([])]
def pack(map, options) do
[format(map) | map |> Map.to_list() |> do_pack([], options)]
end

defp pack([{key, value} | rest], result) do
pack(rest, [@protocol.pack(key), @protocol.pack(value) | result])
defp do_pack([{key, value} | rest], result, opts) do
do_pack(rest, [@protocol.pack(key, opts), @protocol.pack(value, opts) | result], opts)
end

defp pack([], result), do: result
defp do_pack([], result, _opts), do: result

defp format(map) do
length = map_size(map)
Expand All @@ -185,15 +185,15 @@ defimpl Msgpax.Packer, for: Map do
end

defimpl Msgpax.Packer, for: List do
def pack(list) do
[format(list) | list |> Enum.reverse() |> pack([])]
def pack(list, options) do
[format(list) | list |> Enum.reverse() |> do_pack([], options)]
end

defp pack([item | rest], result) do
pack(rest, [@protocol.pack(item) | result])
defp do_pack([item | rest], result, opts) do
do_pack(rest, [@protocol.pack(item, opts) | result], opts)
end

defp pack([], result), do: result
defp do_pack([], result, _opts), do: result

defp format(list) do
length = length(list)
Expand All @@ -208,13 +208,13 @@ defimpl Msgpax.Packer, for: List do
end

defimpl Msgpax.Packer, for: Float do
def pack(num) do
def pack(num, _options) do
<<0xCB, num::64-float>>
end
end

defimpl Msgpax.Packer, for: Integer do
def pack(int) when int < 0 do
def pack(int, _options) when int < 0 do
cond do
int >= -32 -> [0x100 + int]
int >= -128 -> [0xD0, 0x100 + int]
Expand All @@ -225,7 +225,7 @@ defimpl Msgpax.Packer, for: Integer do
end
end

def pack(int) do
def pack(int, _options) do
cond do
int < 128 -> [int]
int < 256 -> [0xCC, int]
Expand All @@ -238,7 +238,7 @@ defimpl Msgpax.Packer, for: Integer do
end

defimpl Msgpax.Packer, for: Msgpax.Bin do
def pack(%{data: data}) when is_binary(data), do: [format(data) | data]
def pack(%{data: data}, _options) when is_binary(data), do: [format(data) | data]

defp format(binary) do
size = byte_size(binary)
Expand All @@ -255,7 +255,7 @@ end
defimpl Msgpax.Packer, for: [Msgpax.Ext, Msgpax.ReservedExt] do
require Bitwise

def pack(%_{type: type, data: data}) do
def pack(%_{type: type, data: data}, _options) do
[format(data), Bitwise.band(256 + type, 255) | data]
end

Expand Down Expand Up @@ -304,15 +304,15 @@ defimpl Msgpax.Packer, for: Any do

quote do
defimpl unquote(@protocol), for: unquote(module) do
def pack(struct) do
def pack(struct, options) do
unquote(extractor)
|> @protocol.Map.pack()
|> @protocol.Map.pack(options)
end
end
end
end

def pack(term) do
def pack(term, _options) do
raise Protocol.UndefinedError, protocol: @protocol, value: term
end
end
4 changes: 2 additions & 2 deletions lib/msgpax/reserved_ext.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
defimpl Msgpax.Packer, for: DateTime do
import Bitwise

def pack(datetime) do
def pack(datetime, options) do
-1
|> Msgpax.ReservedExt.new(build_data(datetime))
|> @protocol.Msgpax.ReservedExt.pack()
|> @protocol.Msgpax.ReservedExt.pack(options)
end

defp build_data(datetime) do
Expand Down
4 changes: 2 additions & 2 deletions test/msgpax/ext_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ defmodule Msgpax.ExtTest do
end

defimpl Msgpax.Packer do
def pack(%Sample{seed: seed, size: size}) do
def pack(%Sample{seed: seed, size: size}, options) do
module = if is_list(seed), do: List, else: String

42
|> Msgpax.Ext.new(module.duplicate(seed, size))
|> @protocol.Msgpax.Ext.pack()
|> @protocol.Msgpax.Ext.pack(options)
end
end
end
Expand Down
31 changes: 31 additions & 0 deletions test/msgpax_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ defmodule MsgpaxTest do
defstruct [:name]
end

test "Msgpax.defimpl/3 injects catch all pack/2" do
defmodule Sample do
use Msgpax
alias Msgpax.Packer

defstruct [:name]

defimpl Packer do
def pack(%{name: name}), do: [name]
end
end

assert function_exported?(Msgpax.Packer.MsgpaxTest.Sample, :pack, 1)
assert function_exported?(Msgpax.Packer.MsgpaxTest.Sample, :pack, 2)
end

test "Msgpax.defimpl/2 injects catch all pack/2" do
defmodule RemoteSample do
defstruct [:name]
end

use Msgpax

defimpl Msgpax.Packer, for: RemoteSample do
def pack(%{name: name}), do: [name]
end

assert function_exported?(Msgpax.Packer.MsgpaxTest.RemoteSample, :pack, 1)
assert function_exported?(Msgpax.Packer.MsgpaxTest.RemoteSample, :pack, 2)
end

test "fixstring" do
assert_format build_string(0), <<160>>
assert_format build_string(31), <<191>>
Expand Down