Packages
absinthe
1.0.0
1.11.0
1.10.2
1.10.1
1.10.0
1.9.1
1.9.0
1.8.0
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.8
1.6.7
retired
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.6.0-rc.1
1.6.0-rc.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.5.0-rc.5
1.5.0-rc.4
1.5.0-rc.3
1.5.0-rc.2
1.5.0-rc.1
1.5.0-rc.0
1.5.0-beta.2
1.5.0-beta.1
1.5.0-beta.0
1.5.0-alpha.4
1.5.0-alpha.3
1.5.0-alpha.2
1.5.0-alpha.1
1.5.0-alpha.0
1.4.16
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
retired
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.3
1.4.0-rc.2
1.4.0-rc.1
1.4.0-rc.0
1.4.0-beta.5
1.4.0-beta.4
1.4.0-beta.3
1.4.0-beta.2
1.4.0-beta.1
1.3.2
1.3.1
1.3.0
1.3.0-rc.0
1.3.0-beta.2
1.3.0-beta.1
1.3.0-beta.0
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.0
1.2.0-beta.0
1.2.0-alpha0
1.2.0-alpha.2
1.2.0-alpha.1
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
0.5.2
0.5.1
0.5.0
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.2.3
0.2.2
0.2.1
0.1.0
GraphQL for Elixir
Current section
Files
Jump to
Current section
Files
lib/absinthe/schema/notation.ex
defmodule Absinthe.Schema.Notation do
@moduledoc """
This module contains macros used to build GraphQL types.
See `Absinthe.Schema` for a rough overview of schema building from scratch.
"""
alias Absinthe.Utils
alias Absinthe.Type
alias Absinthe.Schema.Notation.Scope
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__), only: :macros
Module.register_attribute __MODULE__, :absinthe_definitions, accumulate: true
Module.register_attribute(__MODULE__, :absinthe_descriptions, accumulate: true)
@before_compile unquote(__MODULE__).Writer
@desc nil
end
end
Module.register_attribute(__MODULE__, :placement, accumulate: true)
defp handle_desc(identifier) do
quote do
@absinthe_descriptions {unquote(identifier), @desc}
@desc nil
end
end
# OBJECT
@placement {:object, [toplevel: true]}
@doc """
Define an object type.
Adds an `Absinthe.Type.Object` to your schema.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
Basic definition:
```
object :car do
# ...
end
```
Providing a custom name:
```
object :car, name: "CarType" do
# ...
end
```
"""
defmacro object(identifier, attrs \\ [], [do: block]) do
scope(__CALLER__, :object, identifier, attrs, block)
handle_desc(identifier)
end
@placement {:interfaces, [under: :object]}
@doc """
Declare implemented interfaces for an object.
See also `interface/1`, which can be used for one interface,
and `interface/3`, used to define interfaces themselves.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
object :car do
interfaces [:vehicle, :branded]
# ...
end
```
"""
defmacro interfaces(ifaces) when is_list(ifaces) do
env = __CALLER__
check_placement!(env.module, :interfaces)
Scope.put_attribute(env.module, :interfaces, ifaces)
[]
end
@placement {:resolve, [under: [:field]]}
@doc """
Mark a field as deprecated
In most cases you can simply pass the deprecate: "message" attribute. However
when using the block form of a field it can be nice to also use this macro.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
field :foo, :string do
deprecate "Foo will no longer be supported"
end
```
This is how to deprecate other things
```
field :foo, :string do
arg :bar, :integer, deprecate: "This isn't supported either"
end
enum :colors do
value :red
value :blue, deprecate: "This isn't supported"
end
```
"""
defmacro deprecate(msg) do
env = __CALLER__
check_placement!(env.module, :deprecate)
Scope.put_attribute(env.module, :deprecate, msg)
[]
end
@doc """
Declare an implemented interface for an object.
Adds an `Absinthe.Type.Interface` to your schema.
See also `interfaces/1`, which can be used for multiple interfaces,
and `interface/3`, used to define interfaces themselves.
## Examples
```
object :car do
interface :vehicle
# ...
end
```
"""
@placement {:interface_attribute, [under: :object]}
defmacro interface(identifier) do
env = __CALLER__
check_placement!(env.module, :interface_attribute, as: "`interface` (as an attribute)")
Scope.put_attribute(env.module, :interfaces, identifier, accumulate: true)
[]
end
# INTERFACES
@placement {:interface, [toplevel: :true]}
@doc """
Define an interface type.
Adds an `Absinthe.Type.Interface` to your schema.
Also see `interface/1` and `interfaces/1`, which declare
that an object implements one or more interfaces.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
interface :vehicle do
field :wheel_count, :integer
end
object :rally_car do
field :wheel_count, :integer
interface :vehicle
end
```
"""
defmacro interface(identifier, attrs \\ [], [do: block]) do
scope(__CALLER__, :interface, identifier, attrs, block)
handle_desc(identifier)
end
@placement {:resolve_type, [under: [:interface, :union]]}
@doc """
Define a type resolver for a union or interface.
See also:
* `Absinthe.Type.Interface`
* `Absinthe.Type.Union`
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
interface :entity do
# ...
resolve_type fn
%{employee_count: _}, _ ->
:business
%{age: _}, _ ->
:person
end
end
```
"""
defmacro resolve_type(func_ast) do
env = __CALLER__
check_placement!(env.module, :resolve_type)
Scope.put_attribute(env.module, :resolve_type, func_ast)
[]
end
# FIELDS
@placement {:field, [under: [:input_object, :interface, :object]]}
@doc """
Defines a GraphQL field
See `field/4`
"""
defmacro field(identifier, [do: block]) do
scope(__CALLER__, :field, identifier, [], block)
end
defmacro field(identifier, attrs) when is_list(attrs) do
scope(__CALLER__, :field, identifier, attrs, nil)
end
defmacro field(identifier, type) do
scope(__CALLER__, :field, identifier, [type: type], nil)
end
@doc """
Defines a GraphQL field
See `field/4`
"""
defmacro field(identifier, attrs, [do: block]) when is_list(attrs) do
scope(__CALLER__, :field, identifier, attrs, block)
end
defmacro field(identifier, type, [do: block]) do
scope(__CALLER__, :field, identifier, [type: type], block)
end
defmacro field(identifier, type, attrs) do
scope(__CALLER__, :field, identifier, Keyword.put(attrs, :type, type), nil)
end
@doc """
Defines a GraphQL field.
## Placement
#{Utils.placement_docs(@placement)}
`query`, `mutation`, and `subscription` are
all objects under the covers, and thus you'll find `field` definitions under
those as well.
## Examples
```
field :id, :id
field :age, :integer, description: "How old the item is"
field :name, :string do
description "The name of the item"
end
field :location, type: :location
```
"""
defmacro field(identifier, type, attrs, [do: block]) do
scope(__CALLER__, :field, identifier, Keyword.put(attrs, :type, type), block)
end
@placement {:resolve, [under: [:field]]}
@doc """
Defines a resolve function for a field
Specify a 2 arity function to call when resolving a field. Resolve functions
must return either `{:ok, term}` or `{:error, binary | [binary, ...]}`.
You can either hard code a particular anonymous function, or have a function
call that returns a 2 arity anonymous function. See examples for more information.
The first argument to the function are the GraphQL arguments, and the latter
is an `Absinthe.Execution.Field` struct. It is where you can access the GraphQL
context and other execution data.
Note that when using a hard coded anonymous function, the function will not
capture local variables.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
query do
field :person, :person do
resolve &Person.resolve/2
end
end
```
```
query do
field :person, :person do
resolve fn %{id: id}, _ ->
Person.find(id)
end
end
end
```
```
query do
field :person, :person do
resolve lookup(:person)
end
end
def lookup(:person) do
fn %{id: id}, _ ->
Person.find(id)
end
end
```
"""
defmacro resolve(func_ast) do
env = __CALLER__
check_placement!(env.module, :resolve)
Scope.put_attribute(env.module, :resolve, func_ast)
[]
end
@placement {:is_type_of, [under: [:object]]}
@doc """
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro is_type_of(func_ast) do
env = __CALLER__
check_placement!(env.module, :is_type_of)
Scope.put_attribute(env.module, :is_type_of, func_ast)
[]
end
@placement {:arg, [under: [:directive, :field]]}
# ARGS
@doc """
Add an argument.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
field do
arg :size, :integer
arg :name, :string, description: "The desired name"
end
```
"""
defmacro arg(identifier, type, attrs) do
scope(__CALLER__, :arg, identifier, Keyword.put(attrs, :type, type), nil)
end
@doc """
Add an argument.
See `arg/3`
"""
defmacro arg(identifier, attrs) when is_list(attrs) do
scope(__CALLER__, :arg, identifier, attrs, nil)
end
defmacro arg(identifier, type) do
scope(__CALLER__, :arg, identifier, [type: type], nil)
end
# SCALARS
@placement {:scalar, [toplevel: true]}
@doc """
Define a scalar type
A scalar type requires `parse/1` and `serialize/1` functions.
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
scalar :time do
description "ISOz time"
parse &Timex.DateFormat.parse(&1, "{ISOz}")
serialize &Timex.DateFormat.format!(&1, "{ISOz}")
end
```
"""
defmacro scalar(identifier, attrs, [do: block]) do
scope(__CALLER__, :scalar, identifier, attrs, block)
handle_desc(identifier)
end
@doc """
Defines a scalar type
See `scalar/3`
"""
defmacro scalar(identifier, [do: block]) do
scope(__CALLER__, :scalar, identifier, [], block)
handle_desc(identifier)
end
defmacro scalar(identifier, attrs) do
scope(__CALLER__, :scalar, identifier, attrs, nil)
handle_desc(identifier)
end
@placement {:serialize, [under: [:scalar]]}
@doc """
Defines a serialization function for a `scalar` type
The specified `serialize` function is used on outgoing data. It should simply
return the desired external representation.
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro serialize(func_ast) do
env = __CALLER__
check_placement!(env.module, :serialize)
Scope.put_attribute(env.module, :serialize, func_ast)
[]
end
@placement {:parse, [under: [:scalar]]}
@doc """
Defines a parse function for a `scalar` type
The specified `parse` function is used on incoming data to transform it into
an elixir datastructure.
It should return `{:ok, value}` or `{:error, reason}`
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro parse(func_ast) do
env = __CALLER__
check_placement!(env.module, :parse)
Scope.put_attribute(env.module, :parse, func_ast)
[]
end
# DIRECTIVES
@placement {:directive, [toplevel: true]}
@doc """
Defines a directive
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
directive :mydirective do
arg :if, non_null(:boolean), description: "Skipped when true."
on Language.FragmentSpread
on Language.Field
on Language.InlineFragment
instruction fn
%{if: true} ->
:skip
_ ->
:include
end
end
```
"""
defmacro directive(identifier, attrs \\ [], [do: block]) do
scope(__CALLER__, :directive, identifier, attrs, block)
handle_desc(identifier)
end
@placement {:on, [under: :directive]}
@doc """
Declare a directive as operating an a AST node type
See `directive/2`
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro on(ast_node) do
env = __CALLER__
check_placement!(env.module, :on)
ast_node
|> List.wrap
|> Enum.each(fn
value ->
Scope.put_attribute(
env.module,
:on,
value,
accumulate: true
)
end)
[]
end
@placement {:instruction, [under: :directive]}
@doc """
Calculate the instruction for a directive
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro instruction(func_ast) do
env = __CALLER__
check_placement!(env.module, :instruction)
Scope.put_attribute(env.module, :instruction, func_ast)
[]
end
# INPUT OBJECTS
@placement {:input_object, [toplevel: true]}
@doc """
Defines an input object
See `Absinthe.Type.InputObject`
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
input_object :contact_input do
field :email, non_null(:string)
end
```
"""
defmacro input_object(identifier, attrs \\ [], [do: block]) do
scope(__CALLER__, :input_object, identifier, attrs, block)
handle_desc(identifier)
end
# UNIONS
@placement {:union, [toplevel: true]}
@doc """
Defines a union type
See `Absinthe.Type.Union`
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
union :search_result do
description "A search result"
types [:person, :business]
resolve_type fn
%Person{}, _ -> :person
%Business{}, _ -> :business
end
end
```
"""
defmacro union(identifier, attrs \\ [], [do: block]) do
scope(__CALLER__, :union, identifier, attrs, block)
handle_desc(identifier)
end
@placement {:types, [under: [:union]]}
@doc """
Defines the types possible under a union type
See `union/3`
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro types(types) do
env = __CALLER__
check_placement!(env.module, :types)
Scope.put_attribute(env.module, :types, List.wrap(types))
[]
end
# ENUMS
@placement {:enum, [toplevel: true]}
@doc """
Defines an enum type
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro enum(identifier, attrs, [do: block]) do
scope(__CALLER__, :enum, identifier, attrs, block)
handle_desc(identifier)
end
@doc """
Defines an enum type
See `enum/3`
"""
defmacro enum(identifier, [do: block]) do
scope(__CALLER__, :enum, identifier, [], block)
handle_desc(identifier)
end
defmacro enum(identifier, attrs) do
scope(__CALLER__, :enum, identifier, attrs, nil)
handle_desc(identifier)
end
@placement {:value, [under: [:enum]]}
@doc """
Defines a value possible under an enum type
See `enum/3`
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro value(identifier, raw_attrs \\ []) do
env = __CALLER__
check_placement!(env.module, :value)
attrs = raw_attrs
|> Keyword.put(:value, Keyword.get(raw_attrs, :as, identifier))
|> Keyword.delete(:as)
|> add_description(env)
Scope.put_attribute(env.module, :values, {identifier, attrs}, accumulate: true)
[]
end
# GENERAL ATTRIBUTES
@placement {:description, [toplevel: false]}
@doc """
Defines a description
This macro adds a description to any other macro which takes a block.
Note that you can also specify a description by using `@desc` above any item
that can take a description attribute.
## Placement
#{Utils.placement_docs(@placement)}
"""
defmacro description(text_block) do
text = reformat_description(text_block)
check_placement!(__CALLER__.module, :description)
Scope.put_attribute(__CALLER__.module, :description, text)
[]
end
defp reformat_description(text), do: String.strip(text)
# IMPORTS
@placement {:import_types, [toplevel: true]}
@doc """
Import types from another module
Very frequently your schema module will simply have the `query` and `mutation`
blocks, and you'll want to break out your other types into other modules. This
macro imports those types for use the current module
## Placement
#{Utils.placement_docs(@placement)}
## Examples
```
import_types MyApp.Schema.Types
```
"""
defmacro import_types(type_module_ast) do
env = __CALLER__
type_module_ast
|> Macro.expand(env)
|> do_import_types(env)
[]
end
defp do_import_types(type_module, env) when is_atom(type_module) do
for {ident, name} <- type_module.__absinthe_types__ do
if Enum.member?(type_module.__absinthe_exports__, ident) do
put_definition(env.module, %Absinthe.Schema.Notation.Definition{
category: :type,
source: type_module,
identifier: ident,
attrs: [name: name],
file: env.file,
line: env.line})
end
end
for {ident, name} <- type_module.__absinthe_directives__ do
if Enum.member?(type_module.__absinthe_exports__, ident) do
put_definition(env.module, %Absinthe.Schema.Notation.Definition{
category: :directive,
source: type_module,
identifier: ident,
attrs: [name: name],
file: env.file,
line: env.line})
end
end
end
defp do_import_types(type_module, _) do
raise ArgumentError, """
#{type_module} is not a module
This macro must be given a literal module name or a macro which expands to a
literal module name. Variables are not supported at this time.
"""
end
# TYPE UTILITIES
@doc """
Marks a type reference as non null
See `field/3` for examples
"""
defmacro non_null(type) do
quote do
%Absinthe.Type.NonNull{of_type: unquote(type)}
end
end
@doc """
Marks a type reference as a list of the given type
See `field/3` for examples
"""
defmacro list_of(type) do
quote do
%Absinthe.Type.List{of_type: unquote(type)}
end
end
# NOTATION UTILITIES
# Define a notation scope that will accept attributes
@doc false
def scope(env, kind, identifier, attrs, block) do
open_scope(kind, env, identifier, attrs)
# this is probably too simple for now.
block |> expand(env)
close_scope(kind, env, identifier)
[]
end
defp expand(ast, env) do
Macro.prewalk(ast, fn
{:@, _, [{:desc, _, [desc]}]} ->
Module.put_attribute(env.module, :__absinthe_desc__, desc)
{_, _, _} = node -> Macro.expand(node, env)
node -> node
end)
end
@doc false
# Add a `__reference__` to a generated struct
def add_reference(attrs, env, identifier) do
attrs
|> Keyword.put(
:__reference__,
Macro.escape(%{
module: env.module,
identifier: identifier,
location: %{
file: env.file,
line: env.line
}
})
)
end
# After verifying it is valid in the current context, open a new notation
# scope, setting any provided attributes.
defp open_scope(kind, env, identifier, attrs) do
check_placement!(env.module, kind)
attrs = attrs
|> add_reference(env, identifier)
|> add_description(env)
Scope.open(kind, env.module, attrs)
end
defp add_description(attrs, env) do
case Module.get_attribute(env.module, :__absinthe_desc__) do
nil ->
attrs
desc ->
Module.put_attribute(env.module, :__absinthe_desc__, nil)
Keyword.put(attrs, :description, reformat_description(desc))
end
end
# CLOSE SCOPE HOOKS
@unexported_identifiers ~w(query mutation subscription)a
# Close the current scope and return the appropriate
# quoted result for the type of operation.
defp close_scope(:enum, env, identifier) do
close_scope_and_define_type(Type.Enum, env, identifier)
end
defp close_scope(:object, env, identifier) do
close_scope_and_define_type(
Type.Object, env, identifier,
export: !Enum.member?(@unexported_identifiers, identifier)
)
end
defp close_scope(:interface, env, identifier) do
close_scope_and_define_type(Type.Interface, env, identifier)
end
defp close_scope(:union, env, identifier) do
close_scope_and_define_type(Type.Union, env, identifier)
end
defp close_scope(:input_object, env, identifier) do
close_scope_and_define_type(Type.InputObject, env, identifier)
end
defp close_scope(:field, env, identifier) do
close_scope_and_accumulate_attribute(:fields, env, identifier)
end
defp close_scope(:arg, env, identifier) do
close_scope_and_accumulate_attribute(:args, env, identifier)
end
defp close_scope(:scalar, env, identifier) do
close_scope_and_define_type(Type.Scalar, env, identifier)
end
defp close_scope(:directive, env, identifier) do
close_scope_and_define_directive(env, identifier)
end
defp close_scope(_, env, _) do
Scope.close(env)
end
defp close_scope_with_name(mod, identifier, opts \\ []) do
Scope.close(mod).attrs
|> add_name(identifier, opts)
end
defp close_scope_and_define_directive(env, identifier, def_opts \\ []) do
definition = %Absinthe.Schema.Notation.Definition{
category: :directive,
builder: Absinthe.Type.Directive,
identifier: identifier,
attrs: close_scope_with_name(env.module, identifier),
opts: def_opts,
file: env.file,
line: env.line
}
put_definition(env.module, definition)
end
defp close_scope_and_define_type(type_module, env, identifier, def_opts \\ []) do
attrs = close_scope_with_name(env.module, identifier, title: true)
definition = %Absinthe.Schema.Notation.Definition{
category: :type,
builder: type_module,
identifier: identifier,
attrs: attrs,
opts: def_opts,
file: env.file,
line: env.line
}
put_definition(env.module, definition)
end
defp put_definition(module, definition) do
# Why is accumulate true not working here?
# does that only work with the @ form?
definitions = Module.get_attribute(module, :absinthe_definitions) || []
Module.put_attribute(module, :absinthe_definitions, [definition | definitions])
end
defp close_scope_and_accumulate_attribute(attr_name, env, identifier) do
Scope.put_attribute(env.module, attr_name, {identifier, close_scope_with_name(env.module, identifier)}, accumulate: true)
end
@doc false
# Add the default name, if needed, to a struct
def add_name(attrs, identifier, opts \\ []) do
update_in(attrs, [:name], fn
value ->
default_name(identifier, value, opts)
end)
end
# Find the name, or default as necessary
defp default_name(identifier, nil, opts) do
if opts[:title] do
identifier |> Atom.to_string |> Utils.camelize
else
identifier |> Atom.to_string
end
end
defp default_name(_, name, _) do
name
end
@doc false
# Check whether the provided operation is appropriate in the current
# in the current scope context
def check_placement!(mod, usage, opts \\ []) do
rules = Keyword.get(@placement, usage, [])
|> Enum.into(%{})
do_check_placement!(mod, usage, rules, opts)
end
defp do_check_placement!(mod, usage, %{under: parents} = rules, opts) do
case Scope.current(mod) do
%{name: name} ->
if Enum.member?(List.wrap(parents), name) do
do_check_placement!(mod, usage, Map.delete(rules, :under), opts)
else
raise Absinthe.Schema.Notation.Error, only_within(usage, parents, opts)
end
_ ->
raise Absinthe.Schema.Notation.Error, only_within(usage, parents, opts)
end
end
defp do_check_placement!(mod, usage, %{toplevel: true} = rules, opts) do
case Scope.current(mod) do
nil ->
do_check_placement!(mod, usage, Map.delete(rules, :toplevel), opts)
_ ->
ref = opts[:as] || "`#{usage}`"
raise Absinthe.Schema.Notation.Error, "Invalid schema notation: #{ref} must only be used toplevel"
end
end
defp do_check_placement!(mod, usage, %{toplevel: false} = rules, opts) do
case Scope.current(mod) do
nil ->
ref = opts[:as] || "`#{usage}`"
raise Absinthe.Schema.Notation.Error, "Invalid schema notation: #{ref} must not be used toplevel"
_ ->
do_check_placement!(mod, usage, Map.delete(rules, :toplevel), opts)
end
end
defp do_check_placement!(_, _, rules, _) when map_size(rules) == 0 do
:ok
end
# The error message when a macro can only be used within a certain set of
# parent scopes.
defp only_within(usage, parents, opts) do
ref = opts[:as] || "`#{usage}`"
parts = List.wrap(parents)
|> Enum.map(&"`#{&1}`")
|> Enum.join(", ")
"Invalid schema notation: #{ref} must only be used within #{parts}"
end
end