Current section

Files

Jump to
absinthe lib absinthe.ex
Raw

lib/absinthe.ex

defmodule Absinthe do
@moduledoc """
Documentation for the Absinthe package, a toolkit for building GraphQL
APIs with Elixir.
Absinthe aims to handle authoring GraphQL API schemas -- then supporting
their introspection, validation, and execution according to the
[GraphQL specification](https://facebook.github.io/graphql/).
Here are some additional projects you're likely to use in conjunction with
Absinthe to launch an API:
* [Ecto](http://hexdocs.pm/ecto) - a language integrated query and
database wrapper.
* [Phoenix](http://hexdocs.pm/phoenix) - the Phoenix web framework.
* [Plug](http://hexdocs.pm/plug) - a specification and conveniences
for composable modules in between web applications. (An Absinthe-specific
package for Plug and/or Phoenix is on our near-term roadmap.)
* [Poison](http://hexdocs.pm/poison) - JSON serialization
## GraphQL Basics
For a grounding in GraphQL, I recommend you read through the following articles:
* The [GraphQL Introduction](https://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html) and [GraphQL: A data query language](https://code.facebook.com/posts/1691455094417024/graphql-a-data-query-language/) posts from Facebook.
* The [Your First GraphQL Server](https://medium.com/@clayallsopp/your-first-graphql-server-3c766ab4f0a2#.m78ybemas) Medium post by Clay Allsopp. (Note this uses the [JavaScript GraphQL reference implementation](https://github.com/graphql/graphql-js).)
* Other blog posts that pop up. GraphQL is young!
* For the ambitious, the draft [GraphQL Specification](https://facebook.github.io/graphql/).
You may also be interested in how GraphQL is used by [Relay](https://facebook.github.io/relay/), a "JavaScript frameword for building data-driven React applications."
## GraphQL using Absinthe
The first thing you need to do is define a schema, we do this
by using `Absinthe.Schema`.
Here we'll build a basic schema that defines one query field; a
way to retrieve the data for an `item`, given an `id`. Users of
the API can then decide what fields of the `item` they'd like
returned.
```
defmodule App.Schema do
use Absinthe.Schema
@fake_db %{
"foo" => %{id: "foo", name: "Foo", value: 4},
"bar" => %{id: "bar", name: "Bar", value: 5}
}
def query do
%Absinthe.Type.ObjectType{
fields: fields(
item: [
type: :item,
description: "Get an item by ID",
args: args(
id: [type: :id, description: "The ID of the item"]
),
resolve: fn %{id: id}, _ ->
{:ok, Map.get(@fake_db, id)}
end
]
)
}
end
@absinthe :type
def item do
%Absinthe.Type.ObjectType{
description: "A valuable item",
fields: fields(
id: [type: :id],
name: [type: :string, description: "The item's name"],
value: [type: :integer, description: "Recently appraised value"]
)
}
end
end
```
Now we'll execute a query document against it with
`run/2` or `run/3` (which return tuples), or their exception-raising
equivalents, `run!/2` and `run!/3.
Let's get the `name` of an `item` with `id` `"foo"`:
```
\"""
{
item(id: "foo") {
name
}
}
\"""
|> Absinthe.run(App.Schema)
```
Results are returned in a tuple, and are maps with `:data` and/or `:errors` keys, suitable for serialization
back to the client.
```
{:ok, %{data: %{"name" => "Foo"}}}
```
You can also provide values for variables defined in the query document
(supporting, eg, values passed as query string parameters):
```
\"""
query GetItemById($id: ID) {
item(id: $id) {
name
}
}
\"""
|> Absinthe.run(App.Schema, variables: %{id: params[:item_id]})
```
The result, if `params[:item_id]` was `"foo"`, would be the same:
```
{:ok, %{data: %{"name" => "Foo"}}}
```
`run!/2` and `run!/3` operate similarly, except they will raise
`Absinthe.SytaxError` and `Absinthe.ExecutionError` if they cannot
parse/execute the document.
"""
defmodule ExecutionError do
@moduledoc """
An error during execution.
"""
defexception message: "execution failed"
end
defmodule SyntaxError do
@moduledoc """
An error during parsing.
"""
defexception line: nil, errors: "Syntax error"
def message(exception) do
"#{exception.errors} on line #{exception.line}"
end
end
@doc false
@spec tokenize(binary) :: {:ok, [tuple]} | {:error, binary}
def tokenize(input) do
case :absinthe_lexer.string(input |> to_char_list) do
{:ok, tokens, _line_count} -> {:ok, tokens}
other ->
other
end
end
@doc false
@spec parse(binary) :: {:ok, Absinthe.Language.Document.t} | {:error, tuple}
@spec parse(Absinthe.Language.Source.t) :: {:ok, Absinthe.Language.Document.t} | {:error, tuple}
def parse(input) when is_binary(input) do
parse(%Absinthe.Language.Source{body: input})
end
def parse(input) do
case input.body |> tokenize do
{:ok, []} -> {:ok, %Absinthe.Language.Document{}}
{:ok, tokens} -> :absinthe_parser.parse(tokens)
other -> other
end
end
@doc false
@spec parse!(binary) :: Absinthe.Language.Document.t
@spec parse!(Absinthe.Language.Source.t) :: Absinthe.Language.Document.t
def parse!(input) when is_binary(input) do
parse!(%Absinthe.Language.Source{body: input})
end
def parse!(input) do
case parse(input) do
{:ok, result} -> result
{:error, {line_number, _, errs}} -> raise SyntaxError, source: input, line_number: line_number, error: errs
end
end
@doc """
Evaluates a query document against a schema, with options.
## Options
* `:adapter` - The name of the adapter to use. See the `Absinthe.Adapter`
behaviour and the `Absinthe.Adapter.Passthrough` and
`Absinthe.Adapter.LanguageConventions` modules that implement it.
(`Absinthe.Adapter.Passthrough` is the default value for this option.)
* `:operation_name` - If more than one operation is present in the provided
query document, this must be provided to select which operation to execute.
* `:variables` - A map of provided variable values to be used when filling in
arguments in the provided query document.
"""
@spec run(binary | Absinthe.Language.Source.t | Absinthe.Language.Document.t, atom | Absinthe.Schema.t, Keyword.t) :: {:ok, Absinthe.Execution.result_t} | {:error, any}
def run(%Absinthe.Language.Document{} = document, schema, options) do
case execute(schema, document, options) do
{:ok, result} ->
{:ok, result}
other ->
other
end
end
def run(input, schema, options) do
case parse(input) do
{:ok, document} ->
run(document, schema, options)
{:error, {_, :absinthe_parser, _} = err} ->
{:ok, parser_error_result(err)}
other ->
other
end
end
# Build an error result from a parser error
@spec parser_error_result({integer, :absinthe_parser, [char_list]}) :: Execution.result_t
defp parser_error_result({line, :absinthe_parser, msgs}) do
message = msgs |> Enum.map(&to_string/1) |> Enum.join("")
%{errors: [%{message: message, locations: [%{line: line, column: 0}]}]}
end
@doc """
Evaluates a query document against a schema, without options.
## Options
See `run/3` for the available options.
"""
@spec run!(binary | Absinthe.Language.Source.t | Absinthe.Language.Document.t, atom | Absinthe.Schema.t, Keyword.t) :: Absinthe.Execution.result_t
def run!(input, schema, options) do
case run(input, schema, options) do
{:ok, result} -> result
{:error, err} -> raise ExecutionError, message: err
end
end
@doc """
Evaluates a query document against a schema, with options, raising an
`Absinthe.SyntaxErorr` or `Absinthe.ExecutionError` if a problem occurs.
"""
@spec run!(binary | Absinthe.Language.Source.t | Absinthe.Language.Document.t, atom | Absinthe.Schema.t) :: Absinthe.Execution.result_t
def run!(input, schema), do: run!(input, schema, [])
@spec find_schema(Absinthe.Schema.t | atom) :: Absinthe.Schema.t
defp find_schema(schema_module) when is_atom(schema_module), do: schema_module.schema
defp find_schema(schema), do: schema
#
# EXECUTION
#
@spec execute(Absinthe.Schema.t | atom, Absinthe.Language.Document.t, Keyword.t) :: Absinthe.Execution.result_t
defp execute(schema_ref, document, options) do
schema = find_schema(schema_ref)
%Absinthe.Execution{schema: schema, document: document}
|> Absinthe.Execution.run(options)
end
end