defmodule GraphQL.Encoder do
@moduledoc """
Functions to encode `GraphQL.Query` struct into a string
"""
alias GraphQL.{Node, Query, Variable}
@doc """
Encodes a `GraphQL.Query` struct into a GraphQL query body
"""
@spec encode(Query.t()) :: String.t()
def encode(%Query{} = query) do
has_fragments? = valid?(query.fragments)
has_variables? = valid?(query.variables)
identation = 0
[
query.operation,
" ",
query.name,
if(has_variables?, do: encode_variables(query.variables)),
" {\n",
encode_nodes(query.fields, identation + 2),
"\n}",
if(has_fragments?, do: "\n"),
if(has_fragments?, do: encode_nodes(query.fragments, identation))
]
|> Enum.join()
end
# Variables
defp encode_variables(variables) do
[
"(",
variables |> Enum.map(&encode_variable/1) |> Enum.join(", "),
")"
]
|> Enum.join()
end
defp encode_variable(%Variable{} = var) do
has_default? = var.default_value != nil
[
"$",
var.name,
": ",
var.type,
if(has_default?, do: " = "),
encode_value(var.default_value)
]
|> Enum.join()
end
defp encode_nodes(nil, _), do: ""
defp encode_nodes([], _), do: ""
defp encode_nodes(fields, identation) do
fields
|> Enum.map(&encode_node(&1, identation))
|> Enum.join("\n")
end
# Field
defp encode_node(%Node{node_type: :field} = a_node, identation) do
has_arguments? = valid?(a_node.arguments)
has_nodes? = valid?(a_node.nodes)
has_directives? = valid?(a_node.directives)
[
String.duplicate(" ", identation),
encode_field_alias(a_node.alias),
encode_name(a_node.name),
if(has_arguments?, do: encode_arguments(a_node.arguments)),
if(has_directives?, do: " "),
if(has_directives?, do: encode_directives(a_node.directives)),
if(has_nodes?, do: " {\n"),
encode_nodes(a_node.nodes, identation + 2),
if(has_nodes?, do: "\n"),
if(has_nodes?, do: String.duplicate(" ", identation)),
if(has_nodes?, do: "}")
]
|> Enum.join()
end
# Fragment reference
defp encode_node(%Node{node_type: :fragment_ref} = a_node, identation) do
[
String.duplicate(" ", identation),
"...",
a_node.name
]
|> Enum.join()
end
# Fragment
defp encode_node(%Node{node_type: :fragment} = fragment, identation) do
[
String.duplicate(" ", identation),
"fragment ",
fragment.name,
" on ",
fragment.type,
" {\n",
fragment.nodes |> Enum.map(&encode_node(&1, identation + 2)) |> Enum.join("\n"),
"\n",
String.duplicate(" ", identation),
"}"
]
|> Enum.join()
end
# Inline Fragment
defp encode_node(%Node{node_type: :inline_fragment} = a_node, identation) do
[
String.duplicate(" ", identation),
"... on ",
a_node.type,
" {\n",
a_node.nodes |> Enum.map(&encode_node(&1, identation + 2)) |> Enum.join("\n"),
"\n",
String.duplicate(" ", identation),
"}"
]
|> Enum.join()
end
defp encode_name(name) when is_atom(name), do: Atom.to_string(name)
defp encode_name(name) when is_binary(name), do: name
defp encode_field_alias(nil), do: ""
defp encode_field_alias(an_alias), do: "#{an_alias}: "
# Arguments
def encode_arguments(nil), do: ""
def encode_arguments([]), do: ""
def encode_arguments(map_or_keyword) do
vars =
map_or_keyword
|> Enum.map(&encode_argument/1)
|> Enum.join(", ")
"(#{vars})"
end
def encode_argument({key, value}) do
"#{key}: #{encode_value(value)}"
end
defp encode_value(v) do
cond do
is_binary(v) ->
"\"#{v}\""
is_list(v) ->
v
|> Enum.map(&encode_value/1)
|> Enum.join()
is_map(v) ->
parsed_v =
v
|> Enum.map(&encode_argument/1)
|> Enum.join(", ")
Enum.join(["{", parsed_v, "}"])
true ->
case v do
{:enum, v} -> v
v -> "#{v}"
end
end
end
defp encode_directives(directives) do
directives
|> Enum.map(&encode_directive/1)
|> Enum.join(" ")
end
defp encode_directive({key, arguments}) do
[
"@",
key,
encode_arguments(arguments)
]
|> Enum.join()
end
defp encode_directive(key) do
["@", key] |> Enum.join()
end
defp valid?(nil), do: false
defp valid?([]), do: false
defp valid?(a_map) when is_map(a_map), do: a_map != %{}
defp valid?(_), do: true
end