From 716b8b2346e6f24b5584ddf7f00c6651d04f4a82 Mon Sep 17 00:00:00 2001 From: y86 Date: Tue, 22 Aug 2023 12:24:57 -0300 Subject: [PATCH 1/3] Change pack/1 to pack/2 so it can accept options --- lib/msgpax/fragment.ex | 2 +- lib/msgpax/packer.ex | 52 +++++++++++++++++++------------------- lib/msgpax/reserved_ext.ex | 4 +-- test/msgpax/ext_test.exs | 4 +-- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/msgpax/fragment.ex b/lib/msgpax/fragment.ex index 94d8278..8af79b4 100644 --- a/lib/msgpax/fragment.ex +++ b/lib/msgpax/fragment.ex @@ -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 diff --git a/lib/msgpax/packer.ex b/lib/msgpax/packer.ex index 29dab7f..714c60f 100644 --- a/lib/msgpax/packer.ex +++ b/lib/msgpax/packer.ex @@ -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. @@ -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 @@ -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) @@ -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) @@ -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] @@ -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] @@ -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) @@ -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 @@ -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 diff --git a/lib/msgpax/reserved_ext.ex b/lib/msgpax/reserved_ext.ex index b7dd2a8..ad8320f 100644 --- a/lib/msgpax/reserved_ext.ex +++ b/lib/msgpax/reserved_ext.ex @@ -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 diff --git a/test/msgpax/ext_test.exs b/test/msgpax/ext_test.exs index d4d5a57..70e9627 100644 --- a/test/msgpax/ext_test.exs +++ b/test/msgpax/ext_test.exs @@ -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 From 6a7af6083daf8ca0572e3c751894c2a412926042 Mon Sep 17 00:00:00 2001 From: y86 Date: Tue, 22 Aug 2023 12:25:43 -0300 Subject: [PATCH 2/3] Add defimpl replacent to include catchall pack/2 --- lib/msgpax.ex | 77 ++++++++++++++++++++++++++++++++++++++++++-- test/msgpax_test.exs | 31 ++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/msgpax.ex b/lib/msgpax.ex index 0fbfd0c..6eda7b2 100644 --- a/lib/msgpax.ex +++ b/lib/msgpax.ex @@ -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") @@ -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}} @@ -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 diff --git a/test/msgpax_test.exs b/test/msgpax_test.exs index 53302ad..c6efa87 100644 --- a/test/msgpax_test.exs +++ b/test/msgpax_test.exs @@ -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>> From 19be6fdb5525a584f5ef25a1b50cd6ebb7c3a56b Mon Sep 17 00:00:00 2001 From: y86 Date: Tue, 22 Aug 2023 12:26:04 -0300 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e38a8..62d44e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.