diff --git a/CHANGELOG b/CHANGELOG index 17f4a81..d1606d2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ # 2.0.0 (unreleased) - BREAKING CHANGE: Remove generated defs/0 function - BREAKING CHANGE: Remove Protox.Encode.encode/1 and Protox.Encode.encode!/1 + - BREAKING CHANGE: The JSON library is now configurable via the `:json_library` option at compile time - Drop support for Elixir < 1.15 # 1.7.8 diff --git a/lib/protox.ex b/lib/protox.ex index fc7c8f4..8bd45d0 100644 --- a/lib/protox.ex +++ b/lib/protox.ex @@ -159,32 +159,21 @@ defmodule Protox do - `input` could not be decoded to JSON; `reason` is a `Protox.JsonDecodingError` error ## JSON library configuration - The default library to decode JSON is [`Jason`](https://github.com/michalmuskala/jason). - However, you can chose to use [`Poison`](https://github.com/devinus/poison): - iex> Protox.json_decode("{\\"a\\":\\"BAR\\"}", Namespace.Fiz.Foo, json_decoder: Poison) - {:ok, %Namespace.Fiz.Foo{__uf__: [], a: :BAR, b: %{}}} - - You can also use another library as long as it exports an `decode!` function. You can easily - create a module to wrap a library that would not have this interface (like [`jiffy`](https://github.com/davisp/jiffy)): - defmodule Jiffy do - def decode!(input) do - :jiffy.decode(input, [:return_maps, :use_nil]) - end - end + TODO: document json library configuration """ - @doc since: "1.6.0" - @spec json_decode(iodata(), atom(), keyword()) :: {:ok, struct()} | {:error, any()} - def json_decode(input, message_module, opts \\ []) do - message_module.json_decode(input, opts) + @doc since: "2.0.0" + @spec json_decode(iodata(), atom()) :: {:ok, struct()} | {:error, any()} + def json_decode(input, message_module) do + message_module.json_decode(input) end @doc """ Throwing version of `json_decode/2`. """ - @doc since: "1.6.0" - @spec json_decode!(iodata(), atom(), keyword()) :: struct() | no_return() - def json_decode!(input, message_module, opts \\ []) do - message_module.json_decode!(input, opts) + @doc since: "2.0.0" + @spec json_decode!(iodata(), atom()) :: struct() | no_return() + def json_decode!(input, message_module) do + message_module.json_decode!(input) end @doc """ @@ -211,36 +200,25 @@ defmodule Protox do "{\\"msgK\\":{\\"2\\":\\"bar\\",\\"1\\":\\"foo\\"}}" ## JSON library configuration - The default library to encode values (i.e. mostly to escape strings) to JSON is [`Jason`](https://github.com/michalmuskala/jason). - However, you can chose to use [`Poison`](https://github.com/devinus/poison): - iex> msg = %Namespace.Fiz.Foo{a: :BAR} - iex> Protox.json_encode(msg, json_encoder: Poison) - {:ok, ["{", ["\\"a\\"", ":", "\\"BAR\\""], "}"]} - - You can also use another library as long as it exports an `encode!` function, which is expected to return objects as maps and `nil` - to represent `null`. - You can easily create a module to wrap a library that would not have this interface (like [`jiffy`](https://github.com/davisp/jiffy)): - defmodule Jiffy do - defdelegate encode!(msg), to: :jiffy, as: :encode - end + TODO: document the json library configuration ## Encoding specifications See https://developers.google.com/protocol-buffers/docs/proto3#json for the specifications of the encoding. """ - @doc since: "1.6.0" - @spec json_encode(struct(), keyword()) :: {:ok, iodata()} | {:error, any()} - def json_encode(msg, opts \\ []) do - msg.__struct__.json_encode(msg, opts) + @doc since: "2.0.0" + @spec json_encode(struct()) :: {:ok, iodata()} | {:error, any()} + def json_encode(msg) do + msg.__struct__.json_encode(msg) end @doc """ Throwing version of `json_encode/1`. """ - @doc since: "1.6.0" - @spec json_encode!(struct(), keyword()) :: iodata() | no_return() - def json_encode!(msg, opts \\ []) do - msg.__struct__.json_encode!(msg, opts) + @doc since: "2.0.0" + @spec json_encode!(struct()) :: iodata() | no_return() + def json_encode!(msg) do + msg.__struct__.json_encode!(msg) end # -- Private diff --git a/lib/protox/define_message.ex b/lib/protox/define_message.ex index 33613e5..348a653 100644 --- a/lib/protox/define_message.ex +++ b/lib/protox/define_message.ex @@ -5,6 +5,7 @@ defmodule Protox.DefineMessage do def define(messages, opts \\ []) do keep_unknown_fields = Keyword.get(opts, :keep_unknown_fields, true) + json_library = Keyword.get(opts, :json_library, Jason) for msg = %Protox.Message{} <- messages do fields = Enum.sort(msg.fields, &(&1.tag < &2.tag)) @@ -16,7 +17,7 @@ defmodule Protox.DefineMessage do unknown_fields_funs = make_unknown_fields_funs(unknown_fields, keep_unknown_fields) required_fields_fun = make_required_fields_fun(required_fields) fields_access_funs = make_fields_access_funs(fields) - json_funs = make_json_funs(msg.name) + json_funs = make_json_funs(msg.name, json_library) default_fun = make_default_funs(fields) syntax_fun = make_syntax_fun(msg.syntax) file_options_fun = make_file_options_fun(msg) @@ -93,44 +94,42 @@ defmodule Protox.DefineMessage do end end - defp make_json_funs(msg_name) do + defp make_json_funs(msg_name, json_library) do + json_library_wrapper = Protox.JsonLibrary.get_wrapper(json_library) + quote do - @spec json_decode(iodata(), keyword()) :: {:ok, struct()} | {:error, any()} - def json_decode(input, opts \\ []) do + @spec json_decode(iodata()) :: {:ok, struct()} | {:error, any()} + def json_decode(input) do try do - {:ok, json_decode!(input, opts)} + {:ok, json_decode!(input)} rescue e in Protox.JsonDecodingError -> {:error, e} end end - @spec json_decode!(iodata(), keyword()) :: struct() | no_return() - def json_decode!(input, opts \\ []) do - json_library_wrapper = Protox.JsonLibrary.get_library(opts, :decode) - + @spec json_decode!(iodata()) :: struct() | no_return() + def json_decode!(input) do Protox.JsonDecode.decode!( input, unquote(msg_name), - &json_library_wrapper.decode!(&1) + &unquote(json_library_wrapper).decode!(&1) ) end - @spec json_encode(struct(), keyword()) :: {:ok, iodata()} | {:error, any()} - def json_encode(msg, opts \\ []) do + @spec json_encode(struct()) :: {:ok, iodata()} | {:error, any()} + def json_encode(msg) do try do - {:ok, json_encode!(msg, opts)} + {:ok, json_encode!(msg)} rescue e in Protox.JsonEncodingError -> {:error, e} end end - @spec json_encode!(struct(), keyword()) :: iodata() | no_return() - def json_encode!(msg, opts \\ []) do - json_library_wrapper = Protox.JsonLibrary.get_library(opts, :encode) - - Protox.JsonEncode.encode!(msg, &json_library_wrapper.encode!(&1)) + @spec json_encode!(struct()) :: iodata() | no_return() + def json_encode!(msg) do + Protox.JsonEncode.encode!(msg, &unquote(json_library_wrapper).encode!(&1)) end end end diff --git a/lib/protox/errors.ex b/lib/protox/errors.ex index 126571c..c9b9e5a 100644 --- a/lib/protox/errors.ex +++ b/lib/protox/errors.ex @@ -91,6 +91,21 @@ defmodule Protox.JsonEncodingError do end end +defmodule Protox.JsonLibraryError do + @moduledoc """ + This error is thrown when the configured JSON library is not available. + """ + + defexception message: "" + + @doc false + def new() do + %__MODULE__{ + message: "Cannot load JSON library. Please check your project dependencies." + } + end +end + defmodule Protox.RequiredFieldsError do @moduledoc """ This error is thrown when encoding or decoding a Protobuf 2 message diff --git a/lib/protox/jason.ex b/lib/protox/jason.ex index 532eac3..fc9f53c 100644 --- a/lib/protox/jason.ex +++ b/lib/protox/jason.ex @@ -2,45 +2,23 @@ defmodule Protox.Jason do @moduledoc false @behaviour Protox.JsonLibrary - if Code.ensure_loaded?(Jason) do - @impl true - def load(), do: :ok - - @impl true - def decode!(iodata) do - try do - Jason.decode!(iodata) - rescue - e in Jason.DecodeError -> - reraise Protox.JsonDecodingError.new(Exception.message(e)), __STACKTRACE__ - end - end - - @impl true - def encode!(term) do - try do - Jason.encode!(term) - rescue - e in Jason.EncodeError -> - reraise Protox.JsonEncodingError.new(Exception.message(e)), __STACKTRACE__ - end - end - else - @impl true - def load(), do: :error - - @impl true - def decode!(_iodata) do - raise Protox.JsonDecodingError.new( - "Jason library not loaded. Please check your project dependencies." - ) + @impl true + def decode!(iodata) do + try do + Jason.decode!(iodata) + rescue + e in [Jason.DecodeError, Protocol.UndefinedError] -> + reraise Protox.JsonDecodingError.new(Exception.message(e)), __STACKTRACE__ end + end - @impl true - def encode!(_term) do - raise Protox.JsonEncodingError.new( - "Jason library not loaded. Please check your project dependencies." - ) + @impl true + def encode!(term) do + try do + Jason.encode!(term) + rescue + e in Jason.EncodeError -> + reraise Protox.JsonEncodingError.new(Exception.message(e)), __STACKTRACE__ end end end diff --git a/lib/protox/json_library.ex b/lib/protox/json_library.ex index 3b9f394..cd6f4cd 100644 --- a/lib/protox/json_library.ex +++ b/lib/protox/json_library.ex @@ -3,8 +3,6 @@ defmodule Protox.JsonLibrary do The behaviour to implement when wrapping a JSON library. """ - @callback load() :: :ok | :error - @doc """ Should wrap any exception of the underlying library in Protox.JsonDecodingError. """ @@ -16,21 +14,11 @@ defmodule Protox.JsonLibrary do @callback encode!(term()) :: iodata() | no_return() @doc false - def get_library(opts, decoding_or_encoding) do - json_library_name = Keyword.get(opts, :json_library, Jason) - json_library_wrapper = Module.concat(Protox, json_library_name) - - case json_library_wrapper.load() do - :ok -> - json_library_wrapper - - :error -> - message = "cannot load JSON library. Please check your project dependencies." - - case decoding_or_encoding do - :decode -> raise Protox.JsonDecodingError.new(message) - :encode -> raise Protox.JsonEncodingError.new(message) - end + def get_wrapper(json_library) do + if Code.ensure_loaded?(json_library) do + Module.concat(Protox, json_library) + else + raise Protox.JsonLibraryError.new() end end end diff --git a/lib/protox/poison.ex b/lib/protox/poison.ex index 92e7111..d4fc3e6 100644 --- a/lib/protox/poison.ex +++ b/lib/protox/poison.ex @@ -2,45 +2,23 @@ defmodule Protox.Poison do @moduledoc false @behaviour Protox.JsonLibrary - if Code.ensure_loaded?(Poison) do - @impl true - def load(), do: :ok - - @impl true - def decode!(iodata) do - try do - Poison.decode!(iodata) - rescue - e in [Poison.DecodeError, Poison.ParseError] -> - reraise Protox.JsonDecodingError.new(Exception.message(e)), __STACKTRACE__ - end - end - - @impl true - def encode!(term) do - try do - Poison.encode!(term) - rescue - e in Poison.EncodeError -> - reraise Protox.JsonEncodingError.new(Exception.message(e)), __STACKTRACE__ - end - end - else - @impl true - def load(), do: :error - - @impl true - def decode!(_iodata) do - raise Protox.JsonDecodingError.new( - "Poison library not loaded. Please check your project dependencies." - ) + @impl true + def decode!(iodata) do + try do + Poison.decode!(iodata) + rescue + e in [Poison.DecodeError, Poison.ParseError] -> + reraise Protox.JsonDecodingError.new(Exception.message(e)), __STACKTRACE__ end + end - @impl true - def encode!(_term) do - raise Protox.JsonEncodingError.new( - "Poison library not loaded. Please check your project dependencies." - ) + @impl true + def encode!(term) do + try do + Poison.encode!(term) + rescue + e in Poison.EncodeError -> + reraise Protox.JsonEncodingError.new(Exception.message(e)), __STACKTRACE__ end end end diff --git a/test/protox/json_decode_test.exs b/test/protox/json_decode_test.exs index badcf48..b548f8b 100644 --- a/test/protox/json_decode_test.exs +++ b/test/protox/json_decode_test.exs @@ -685,26 +685,24 @@ defmodule Protox.JsonDecodeTest do { :ok, %{ - json: "{\"a\":null, \"b\":\"foo\", \"c\": 33}", - expected: %Sub{a: 0, b: "foo", c: 33} + json: "{\"a\":null, \"b\":\"foo\", \"c\": 33}" } } end - test "Success: jason", %{json: json, expected: expected} do - assert Protox.json_decode!(json, Sub, json_library: Jason) == expected + test "Success: Jason", %{json: json} do + assert Protox.json_decode!(json, WithJason.Sub) == %WithJason.Sub{a: 0, b: "foo", c: 33} end - test "Success: poison", %{json: json, expected: expected} do - assert Protox.json_decode!(json, Sub, json_library: Poison) == expected + test "Success: Poison", %{json: json} do + assert Protox.json_decode!(json, WithPoison.Sub) == %WithPoison.Sub{a: 0, b: "foo", c: 33} end - test "Failure: poison", %{} do + test "Failure: Poison", %{} do assert_raise Protox.JsonDecodingError, fn -> Protox.json_decode!( "{\"mapInt32Int32\": 1", - ProtobufTestMessages.Proto3.TestAllTypesProto3, - json_library: Poison + WithPoison.ProtobufTestMessages.Proto3.TestAllTypesProto3 ) end end diff --git a/test/protox/json_encode_test.exs b/test/protox/json_encode_test.exs index ae57397..ba8bbae 100644 --- a/test/protox/json_encode_test.exs +++ b/test/protox/json_encode_test.exs @@ -393,8 +393,8 @@ defmodule Protox.JsonEncodeTest do describe "JSON libraries" do test "Success: Jason" do - msg = %Msg{msg_k: %{1 => "a", 2 => "b"}} - json = Protox.json_encode!(msg, json_library: Jason) + msg = %WithJason.Msg{msg_k: %{1 => "a", 2 => "b"}} + json = Protox.json_encode!(msg) assert json == [ "{", @@ -404,8 +404,8 @@ defmodule Protox.JsonEncodeTest do end test "Success: Poison" do - msg = %Msg{msg_k: %{1 => "a", 2 => "b"}} - json = Protox.json_encode!(msg, json_library: Poison) + msg = %WithPoison.Msg{msg_k: %{1 => "a", 2 => "b"}} + json = Protox.json_encode!(msg) assert json == [ "{", @@ -415,10 +415,12 @@ defmodule Protox.JsonEncodeTest do end test "Failure: Poison" do - invalid_field = %ProtobufTestMessages.Proto3.TestAllTypesProto3{optional_int32: make_ref()} + invalid_field = %WithPoison.ProtobufTestMessages.Proto3.TestAllTypesProto3{ + optional_int32: make_ref() + } assert_raise Protox.JsonEncodingError, fn -> - Protox.json_encode!(invalid_field, json_library: Poison) + Protox.json_encode!(invalid_field) end end end diff --git a/test/support/messages.ex b/test/support/messages.ex index 320766d..947f5ac 100644 --- a/test/support/messages.ex +++ b/test/support/messages.ex @@ -5,4 +5,20 @@ defmodule Protox.Messages do files: [ "./test/samples/messages.proto" ] + + use Protox, + files: [ + "./test/samples/messages.proto", + "./test/samples/test_messages_proto3.proto" + ], + namespace: WithJason, + json_library: Jason + + use Protox, + files: [ + "./test/samples/messages.proto", + "./test/samples/test_messages_proto3.proto" + ], + namespace: WithPoison, + json_library: Poison end