Skip to content
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
23 changes: 20 additions & 3 deletions lib/absinthe/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ defmodule Absinthe.Plug do
- `:adapter` -- (Optional) Absinthe adapter to use (default: `Absinthe.Adapter.LanguageConventions`).
- `:context` -- (Optional) Initial value for the Absinthe context, available to resolvers. (default: `%{}`).
- `:no_query_message` -- (Optional) Message to return to the client if no query is provided (default: "No query document supplied").
- `:json_codec` -- (Optional) A `module` or `{module, Keyword.t}` dictating which JSON codec should be used (default: `Jason`). The codec module should implement `encode!/2` (e.g., `module.encode!(body, opts)`).
- `:json_codec` -- (Optional) A `module` or `{module, Keyword.t}` dictating which JSON codec should be used (default: `Jason`). The codec module should implement `encode!/1` at minimum. Codecs that accept keyword options (like Jason) can also be configured as `{module, opts}`. Elixir 1.18+'s built-in `JSON` module is supported when passed as a bare module (i.e. `json_codec: JSON`).
- `:pipeline` -- (Optional) `{module, atom}` reference to a 2-arity function that will be called to generate the processing pipeline. (default: `{Absinthe.Plug, :default_pipeline}`).
- `:document_providers` -- (Optional) A `{module, atom}` reference to a 1-arity function that will be called to determine the document providers that will be used to process the request. (default: `{Absinthe.Plug, :default_document_providers}`, which configures `Absinthe.Plug.DocumentProvider.Default` as the lone document provider). A simple list of document providers can also be given. See `Absinthe.Plug.DocumentProvider` for more information about document providers, their role in processing requests, and how you can define and configure your own.
- `:schema` -- (Required, if not handled by Mix.Config) The Absinthe schema to use. If a module name is not provided, `Application.get_env(:absinthe, :schema)` will be attempt to find one.
Expand Down Expand Up @@ -602,12 +602,29 @@ defmodule Absinthe.Plug do
}) do
conn
|> put_resp_content_type(content_type)
|> send_resp(status, mod.encode!(body, opts))
|> send_resp(status, safe_encode!(mod, body, opts))
end

@doc false
def encode_json!(value, %{json_codec: json_codec}) do
json_codec.module.encode!(value, json_codec.opts)
safe_encode!(json_codec.module, value, json_codec.opts)
end

# Safely encode a value using the given codec module and options.
#
# Elixir 1.18's built-in JSON module has a different `encode!/2` signature
# than Jason: `JSON.encode!/2` expects an encoder function as the second
# argument, not a keyword list of options. To stay compatible with both
# conventions, we call `encode!/1` when opts is empty, and only pass opts
# through when they are non-empty (which means the user chose a codec like
# Jason that actually supports keyword options).
@doc false
def safe_encode!(module, value, opts) when opts == [] do
module.encode!(value)
end

def safe_encode!(module, value, opts) do
module.encode!(value, opts)
end

@doc false
Expand Down
30 changes: 26 additions & 4 deletions lib/absinthe/plug/graphiql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,12 @@ defmodule Absinthe.Plug.GraphiQL do

var_string =
variables
|> config.json_codec.module.encode!(pretty: true)
|> pretty_encode!(config.json_codec.module)
|> js_escape

result =
result
|> config.json_codec.module.encode!(pretty: true)
|> pretty_encode!(config.json_codec.module)
|> js_escape

config =
Expand All @@ -274,7 +274,7 @@ defmodule Absinthe.Plug.GraphiQL do

var_string =
variables
|> config.json_codec.module.encode!(pretty: true)
|> pretty_encode!(config.json_codec.module)
|> js_escape

config =
Expand Down Expand Up @@ -408,7 +408,7 @@ defmodule Absinthe.Plug.GraphiQL do
header_string =
val
|> Enum.map(fn {k, v} -> %{"name" => k, "value" => v} end)
|> config.json_codec.module.encode!(pretty: true)
|> pretty_encode!(config.json_codec.module)

Map.put(config, :default_headers, header_string)

Expand Down Expand Up @@ -473,4 +473,26 @@ defmodule Absinthe.Plug.GraphiQL do
defp normalize_socket_url(%{socket_url: url} = config, _) do
%{config | socket_url: "'#{url}'"}
end

# Encode a value with pretty-printing when the codec supports keyword options.
# Jason and Poison accept `encode!(value, pretty: true)`, but Elixir 1.18's
# built-in JSON module uses a different `encode!/2` signature where the second
# argument is an encoder function, not keyword options.
defp pretty_encode!(value, module) do
if supports_keyword_opts?(module) do
module.encode!(value, pretty: true)
else
module.encode!(value)
end
end

@supported_keyword_codecs [Jason, Poison]

defp supports_keyword_opts?(module) when module in @supported_keyword_codecs, do: true

defp supports_keyword_opts?(module) do
# For unknown codecs, check if encode!/2 exists and is not the Elixir
# built-in JSON module (which uses encode!/2 with a function argument).
function_exported?(module, :encode!, 2) and module != JSON
end
end
57 changes: 57 additions & 0 deletions test/lib/absinthe/plug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,61 @@ defmodule Absinthe.PlugTest do
defp basic_opts(context) do
Map.put(context, :opts, Absinthe.Plug.init(schema: TestSchema))
end

describe "safe_encode!/3" do
test "calls encode!/1 when opts is empty" do
assert Absinthe.Plug.safe_encode!(Jason, %{a: 1}, []) == ~s({"a":1})
end

test "calls encode!/2 when opts is non-empty" do
result = Absinthe.Plug.safe_encode!(Jason, %{a: 1}, pretty: true)
assert result =~ "\"a\""
assert result =~ "\n"
end

if Code.ensure_loaded?(JSON) do
test "works with Elixir 1.18 built-in JSON module and empty opts" do
assert Absinthe.Plug.safe_encode!(JSON, %{a: 1}, []) == ~s({"a":1})
end
end
end

if Code.ensure_loaded?(JSON) do
describe "Elixir 1.18 JSON codec integration" do
@query """
{
item(id: "foo") {
name
}
}
"""

test "works as json_codec for basic queries" do
opts = Absinthe.Plug.init(schema: TestSchema, json_codec: JSON)

assert %{status: 200, resp_body: resp_body} =
conn(:post, "/", @query)
|> put_req_header("content-type", "application/graphql")
|> plug_parser
|> Absinthe.Plug.call(opts)

assert Jason.decode!(resp_body) == %{"data" => %{"item" => %{"name" => "Foo"}}}
end

test "works as json_codec with JSON content type" do
opts = Absinthe.Plug.init(schema: TestSchema, json_codec: JSON)

query_str = ~S|{ item(id: "foo") { name } }|
body = JSON.encode!(%{query: query_str})

assert %{status: 200, resp_body: resp_body} =
conn(:post, "/", body)
|> put_req_header("content-type", "application/json")
|> plug_parser
|> Absinthe.Plug.call(opts)

assert Jason.decode!(resp_body) == %{"data" => %{"item" => %{"name" => "Foo"}}}
end
end
end
end