Skip to content
Merged
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
22 changes: 20 additions & 2 deletions lib/tesla/middleware/json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ defmodule Tesla.Middleware.JSON do
mix deps.compile tesla
```

> #### Using built-in `JSON` from Elixir 1.18 {: .info}
>
> This middleware supports the built-in `JSON` module introduced in ELixir 1.18, but for historical
> reasons is it not the default. To use it, set it as the `:engine`:
>
> {Tesla.Middleware.JSON, engine: JSON}
>
> For more advanced usage using custom encoders/decodes, provide the `:encode` and `:decode` anonymous functions instead.

If you only need to encode the request body or decode the response body,
you can use `Tesla.Middleware.EncodeJson` or `Tesla.Middleware.DecodeJson` directly instead.

Expand All @@ -24,6 +33,8 @@ defmodule Tesla.Middleware.JSON do
# use jason engine
Tesla.Middleware.JSON,
# or
{Tesla.Middleware.JSON, engine: JSON}
# or
{Tesla.Middleware.JSON, engine: JSX, engine_opts: [strict: [:comments]]},
# or
{Tesla.Middleware.JSON, engine: Poison, engine_opts: [keys: :atoms]},
Expand All @@ -39,7 +50,7 @@ defmodule Tesla.Middleware.JSON do
- `:decode` - decoding function
- `:encode` - encoding function
- `:encode_content_type` - content-type to be used in request header
- `:engine` - encode/decode engine, e.g `Jason`, `Poison` or `JSX` (defaults to Jason)
- `:engine` - encode/decode engine, e.g `JSON`, `Jason`, `Poison` or `JSX` (defaults to Jason)
- `:engine_opts` - optional engine options
- `:decode_content_types` - list of additional decodable content-types
"""
Expand Down Expand Up @@ -172,7 +183,14 @@ defmodule Tesla.Middleware.JSON do
if fun = opts[op] do
fun.(data)
else
engine = Keyword.get(opts, :engine, @default_engine)
engine =
case Keyword.fetch(opts, :engine) do
# Special case for JSON, which doesn't have encode/2 nor return {:ok, json}
{:ok, JSON} -> Tesla.Middleware.JSON.JSONAdapter
{:ok, engine} -> engine
:error -> @default_engine
end

opts = Keyword.get(opts, :engine_opts, [])

apply(engine, op, [data, opts])
Expand Down
18 changes: 18 additions & 0 deletions lib/tesla/middleware/json/json_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
if Code.ensure_loaded?(JSON) do
defmodule Tesla.Middleware.JSON.JSONAdapter do
@moduledoc false
# An adapter for Elixir's built-in JSON module introduced in Elixir 1.18
# that adjusts for Tesla's assumptions about a JSON engine, which are not satisfied by
# Elixir's JSON module. The assumptions are:
# - the module provides encode/2 and decode/2 functions
# - the 2nd argument to the functions is opts
# - the functions return {:ok, json} or {:error, reason} - not the case for JSON.encode!/2
#
# We do not support custom encoders and decoders.
# The purpose of this adapter is to allow `engine: JSON` to be set easily. If more advanced
# customization is required, the `:encode` and `:decode` functions can be supplied to the
# middleware instead of the `:engine` option.
def encode(data, _opts), do: {:ok, JSON.encode!(data)}
def decode(binary, _opts), do: JSON.decode(binary)
end
end
23 changes: 23 additions & 0 deletions test/tesla/middleware/json/json_adapter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
if Code.ensure_loaded?(JSON) do
defmodule Tesla.Middleware.JSON.JSONAdapterTest do
use ExUnit.Case, async: true

alias Tesla.Middleware.JSON.JSONAdapter

describe "encode/2" do
test "encodes as expected with default encoder" do
assert {:ok, ~S({"hello":"world"})} == JSONAdapter.encode(%{hello: "world"}, [])
end
end

describe "decode/2" do
test "returns {:ok, term} on success" do
assert {:ok, %{"hello" => "world"}} = JSONAdapter.decode(~S({"hello":"world"}), [])
end

test "returns {:error, reason} on failure" do
assert {:error, {:invalid_byte, _, _}} = JSONAdapter.decode("invalid_json", [])
end
end
end
end
48 changes: 48 additions & 0 deletions test/tesla/middleware/json_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,52 @@ defmodule Tesla.Middleware.JsonTest do
assert env.body == %{"value" => 123}
end
end

if Code.ensure_loaded?(JSON) do
describe "Engine: JSON" do
defmodule JSONClient do
use Tesla

plug Tesla.Middleware.JSON, engine: JSON

adapter fn env ->
assert env.body == ~S({"hello":"world"})

{:ok,
%{
env
| status: 200,
headers: [{"content-type", "application/json"}],
body: ~s|{"value": 123}|
}}
end
end

test "encodes/decodes as expected" do
assert {:ok, env} = JSONClient.post("/json", %{hello: "world"})
assert env.body == %{"value" => 123}
end

defmodule Decoder do
use Tesla

plug Tesla.Middleware.DecodeJson, engine: JSON

adapter fn env ->
{:ok,
%{
env
| status: 200,
headers: [{"content-type", "application/json"}],
body: ~s|{"value": 123}|
}}
end
end

test "decodes as expected" do
assert {:ok, env} = Decoder.get("/json")
assert env.body == %{"value" => 123}
end
end
end
end