Current section

Files

Jump to
absinthe lib absinthe adapter.ex
Raw

lib/absinthe/adapter.ex

defmodule Absinthe.Adapter do
@moduledoc """
Absinthe supports an adapter mechanism that allows developers to define their
schema using one code convention (eg, `snake_cased` fields and arguments), but
accept query documents and return results (including names in errors) in
another (eg, `camelCase`).
Adapters aren't a part of GraphQL, but a utility that Absinthe adds so that
both client and server can use use conventions most natural to them.
Absinthe ships with two adapters:
* `Absinthe.Adapter.Passthrough`, which is a no-op adapter and makes no
modifications. (This is the default.)
* `Absinthe.Adapter.LanguageConventions`, which expects schemas to be defined
in `snake_case` (the standard Elixir convention), translating to/from `camelCase`
for incoming query documents and outgoing results.
To set the adapter, you can set an application configuration value:
```
config :absinthe,
adapter: Absinthe.Adapter.LanguageConventions
```
Or, you can provide it as an option to `Absinthe.run/3`:
```
Absinthe.run(query, MyApp.Schema,
adapter: Absinthe.Adapter.LanguageConventions)
```
Notably, this means you're able to switch adapters on case-by-case basis.
In a Phoenix application, this means you could even support using different
adapters for different clients.
A custom adapter module must merely implement the `Absinthe.Adapter` protocol,
in many cases with `use Absinthe.Adapter` and only overriding the desired
functions.
## Writing Your Own
Considering the default implementation of the callbacks handle traversing
ASTs for you, there's a good chance all you may need to implement in your
adapter is `to_internal_name/2` and `to_external_name/2`.
Check out `Absinthe.Adapter.LanguageConventions` for a good example.
"""
alias Absinthe.Execution
defmacro __using__(_) do
quote location: :keep do
@behaviour unquote(__MODULE__)
alias Absinthe.Execution
alias Absinthe.Language
def load_document(%{definitions: definitions} = node) do
%{node | definitions: definitions |> Enum.map(&load_ast_node/1)}
end
# Rename a AST node and traverse children
defp load_ast_node(%Language.OperationDefinition{name: name, selection_set: selection_set} = node) do
%{node | name: name |> to_internal_name(:operation), selection_set: load_ast_node(selection_set)}
end
defp load_ast_node(%Language.SelectionSet{selections: selections} = node) do
%{node | selections: selections |> Enum.map(&load_ast_node/1)}
end
defp load_ast_node(%Language.Field{arguments: args, name: name, selection_set: selection_set} = node) do
%{node | name: name |> to_internal_name(:field), selection_set: load_ast_node(selection_set), arguments: args |> Enum.map(&load_ast_node/1)}
end
defp load_ast_node(%Language.Argument{name: name} = node) do
%{node | name: name |> to_internal_name(:argument)}
end
defp load_ast_node(nil) do
nil
end
def dump_results(%{data: data} = results) do
%{results | data: do_dump_results(data)}
end
def dump_results(results) do
results
end
# Rename a result data value and traverse children
defp do_dump_results(node) when is_map(node) do
for {key, val} <- node, into: %{} do
case key do
name when is_binary(name) ->
{to_external_name(name, :result), do_dump_results(val)}
other ->
{key, do_dump_results(val)}
end
end
end
defp do_dump_results([node|rest]) do
[do_dump_results(node)|do_dump_results(rest)]
end
defp do_dump_results(node) do
node
end
def to_internal_name(external_name, _role), do: external_name
def to_external_name(internal_name, _role), do: internal_name
def format_error(%{name: name, role: role, value: value}, locations) when is_function(value) do
external_name = name |> to_external_name(role)
%{
message: value.(external_name) |> to_string,
locations: locations
}
end
def format_error(%{value: value, role: role} = error_info, locations) when is_binary(value) do
role_name = role |> to_string |> String.capitalize
%{error_info | value: &"#{role_name} `#{&1}': #{value}"}
|> format_error(locations)
end
def format_error(%{value: value} = error_info, locations) do
%{error_info | value: inspect(value)}
|> format_error(locations)
end
defoverridable [load_document: 1,
dump_results: 1,
format_error: 2,
to_internal_name: 2,
to_external_name: 2]
end
end
@doc """
Convert the incoming (external) parsed document to the canonical (internal)
representation that matches the schema.
## Examples
```
def load_document(%{definitions: definitions} = document) do
%{document | definitions: definitions |> Enum.map(&your_custom_transformation/1)}
end
```
"""
@callback load_document(Absinthe.Language.Document.t) :: Absinthe.Language.Document.t
@doc """
Convert the canonical (internal) results to the output (external)
representation.
## Examples
```
def dump_results(%{data: data} = results) do
%{results | data: your_custom_transformation(data)}
end
```
"""
@callback dump_results(Absinthe.Execution.result_t) :: any
@typedoc "The lexical role of a name within the document/schema."
@type role_t :: :operation | :field | :argument | :result
@doc """
Convert a name from an external name to an internal name.
## Examples
Prefix all names with their role, just for fun!
```
def to_internal_name(external_name, role) do
role_name = role |> to_string
role_name <> "_" <> external_name
end
```
"""
@callback to_internal_name(binary, role_t) :: binary
@doc """
Convert a name from an internal name to an external name.
## Examples
Remove the role-prefix (the inverse of what we did in `to_internal_name/2` above):
```
def to_external_name(internal_name, role) do
internal_name
|> String.replace(~r/^\#{role}_/, "")
end
```
"""
@callback to_external_name(binary, role_t) :: binary
@doc """
Format an error using `value` for `name` located at the provided line/column
locations.
## Examples
Here's what the default implementation does:
iex> format_error(%{name: "foo", role: :field, value: &"missing value '\#{&1}'"}, [%{line: 2, column: 4}])
%{message: "missing value 'foo'", locations: [%{line: 2, column: 4}]}
iex> format_error(%{name: "foo", role: :field, value: "missing value"}, [%{line: 2, column: 4}])
%{message: "Field 'foo': missing value", locations: [%{line: 2, column: 4}]}
"""
@callback format_error(Execution.error_info_t, [Execution.error_location_t]) :: Execution.error_t
end