Skip to content
This repository has been archived by the owner on Jul 25, 2024. It is now read-only.

Commit

Permalink
Merge pull request #25 from joshprice/execute-queries
Browse files Browse the repository at this point in the history
Query execution in the style of reference implementation
  • Loading branch information
Josh Price committed Dec 2, 2015
2 parents 7a8a19b + e4d64b6 commit c9c6a59
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 198 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ defmodule TestSchema do
query: %GraphQL.ObjectType{
name: "RootQueryType",
fields: [
%GraphQL.FieldDefinition{
name: "greeting",
type: "String",
resolve: &greeting/1,
}
%GraphQL.FieldDefinition{name: "greeting", type: "String", resolve: &greeting/1}
]
}
}
Expand Down
100 changes: 7 additions & 93 deletions lib/graphql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,6 @@ defmodule GraphQL do
The `GraphQL` module provides a
[GraphQL](http://facebook.github.io/graphql/) implementation for Elixir.
## Parse a query
Parse a GraphQL query
iex> GraphQL.parse "{ hello }"
{:ok, %{definitions: [
%{kind: :OperationDefinition, loc: %{start: 0},
operation: :query,
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
selections: [
%{kind: :Field, loc: %{start: 0}, name: "hello"}
]
}}
],
kind: :Document, loc: %{start: 0}
}}
## Execute a query
Execute a GraphQL query against a given schema / datastore.
Expand All @@ -30,62 +13,12 @@ defmodule GraphQL do
# {:ok, %{hello: "world"}}
"""

alias GraphQL.Schema
alias GraphQL.SyntaxError

defmodule ObjectType do
defstruct name: "RootQueryType", description: "", fields: []
defstruct name: "RootQueryType", description: "", fields: %{}
end

defmodule FieldDefinition do
defstruct name: nil, type: "String", resolve: nil
end

@doc """
Tokenize the input string into a stream of tokens.
iex> GraphQL.tokenize("{ hello }")
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
"""
def tokenize(input_string) when is_binary(input_string) do
input_string |> to_char_list |> tokenize
end

def tokenize(input_string) do
{:ok, tokens, _} = :graphql_lexer.string input_string
tokens
end

@doc """
Parse the input string into a Document AST.
iex> GraphQL.parse("{ hello }")
{:ok,
%{definitions: [
%{kind: :OperationDefinition, loc: %{start: 0},
operation: :query,
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
selections: [
%{kind: :Field, loc: %{start: 0}, name: "hello"}
]
}}
],
kind: :Document, loc: %{start: 0}
}
}
"""
def parse(input_string) when is_binary(input_string) do
input_string |> to_char_list |> parse
end

def parse(input_string) do
case input_string |> tokenize |> :graphql_parser.parse do
{:ok, parse_result} ->
{:ok, parse_result}
{:error, {line_number, _, errors}} ->
{:error, %{errors: [%{message: "GraphQL: #{errors} on line #{line_number}", line_number: line_number}]}}
end
defstruct name: nil, type: "String", args: %{}, resolve: nil
end

@doc """
Expand All @@ -94,31 +27,12 @@ defmodule GraphQL do
# iex> GraphQL.execute(schema, "{ hello }")
# {:ok, %{hello: world}}
"""
def execute(schema, query) do
case parse(query) do
def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do
case GraphQL.Lang.Parser.parse(query) do
{:ok, document} ->
query_fields = hd(document[:definitions])[:selectionSet][:selections]

%Schema{
query: _query_root = %ObjectType{
name: "RootQueryType",
fields: fields
}
} = schema

result = for fd <- fields, qf <- query_fields, qf[:name] == fd.name do
arguments = Map.get(qf, :arguments, [])
|> Enum.map(&parse_argument/1)

{String.to_atom(fd.name), fd.resolve.(arguments)}
end

{:ok, Enum.into(result, %{})}
{:error, error} -> {:error, error}
GraphQL.Execution.Executor.execute(schema, document, root_value, variable_values, operation_name)
{:error, errors} ->
{:error, errors}
end
end

defp parse_argument(%{kind: :Argument, loc: _, name: name, value: %{kind: _, loc: _, value: value}}) do
{String.to_atom(name), value}
end
end
File renamed without changes.
148 changes: 148 additions & 0 deletions lib/graphql/execution/executor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
defmodule GraphQL.Execution.Executor do
@moduledoc ~S"""
Execute a GraphQL query against a given schema / datastore.
# iex> GraphQL.execute schema, "{ hello }"
# {:ok, %{hello: "world"}}
"""

@doc """
Execute a query against a schema.
# iex> GraphQL.execute(schema, "{ hello }")
# {:ok, %{hello: world}}
"""
def execute(schema, document, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do
context = build_execution_context(schema, document, root_value, variable_values, operation_name)
{:ok, {data, _errors}} = execute_operation(context, context.operation, root_value)
{:ok, data}
end

defp build_execution_context(schema, document, root_value, variable_values, operation_name) do
%{
schema: schema,
fragments: %{},
root_value: root_value,
operation: find_operation(document, operation_name),
variable_values: variable_values,
errors: []
}
end

defp execute_operation(context, operation, root_value) do
type = operation_root_type(context.schema, operation)
fields = collect_fields(context, type, operation.selectionSet)
result = case operation.operation do
:mutation -> execute_fields_serially(context, type, root_value, fields)
_ -> execute_fields(context, type, root_value, fields)
end
{:ok, {result, nil}}
end

defp find_operation(document, operation_name) do
if operation_name do
Enum.find(document.definitions, fn(definition) -> definition.name == operation_name end)
else
hd(document.definitions)
end
end

defp operation_root_type(schema, operation) do
Map.get(schema, operation.operation)
end

defp collect_fields(_context, _runtime_type, selection_set, fields \\ %{}, _visited_fragment_names \\ %{}) do
Enum.reduce selection_set[:selections], fields, fn(selection, fields) ->
case selection do
%{kind: :Field} -> Map.put(fields, field_entry_key(selection), [selection])
_ -> fields
end
end
end

# source_value -> root_value?
defp execute_fields(context, parent_type, source_value, fields) do
Enum.reduce fields, %{}, fn({field_name, field_asts}, results) ->
Map.put results, field_name, resolve_field(context, parent_type, source_value, field_asts)
end
end

defp execute_fields_serially(context, parent_type, source_value, fields) do
# call execute_fields because no async operations yet
execute_fields(context, parent_type, source_value, fields)
end

defp resolve_field(context, parent_type, source, field_asts) do
field_ast = hd(field_asts)
field_name = field_ast.name
field_def = field_definition(context.schema, parent_type, field_name)
return_type = field_def.type

resolve_fn = Map.get(field_def, :resolve, &default_resolve_fn/3)
args = argument_values(Map.get(field_def, :args, %{}), Map.get(field_ast, :arguments, %{}), context.variable_values)
info = %{
field_name: field_name,
field_asts: field_asts,
return_type: return_type,
parent_type: parent_type,
schema: context.schema,
fragments: context.fragments,
root_value: context.root_value,
operation: context.operation,
variable_values: context.variable_values
}
result = resolve_fn.(source, args, info)
complete_value_catching_error(context, return_type, field_asts, info, result)
end

defp default_resolve_fn(source, _args, %{field_name: field_name}) do
source[field_name]
end

defp complete_value_catching_error(context, return_type, field_asts, info, result) do
# TODO lots of error checking
complete_value(context, return_type, field_asts, info, result)
end

defp complete_value(context, %GraphQL.ObjectType{} = return_type, field_asts, _info, result) do
sub_field_asts = Enum.reduce field_asts, %{}, fn(field_ast, sub_field_asts) ->
if selection_set = Map.get(field_ast, :selectionSet) do
collect_fields(context, return_type, selection_set, sub_field_asts)
else
sub_field_asts
end
end
execute_fields(context, return_type, result, sub_field_asts)
end

defp complete_value(_context, _return_type, _field_asts, _info, result) do
result
end

defp field_definition(_schema, parent_type, field_name) do
# TODO deal with introspection
parent_type.fields[String.to_atom field_name]
end

defp argument_values(arg_defs, arg_asts, variable_values) do
arg_ast_map = Enum.reduce arg_asts, %{}, fn(arg_ast, result) ->
Map.put(result, String.to_atom(arg_ast.name), arg_ast)
end
Enum.reduce arg_defs, %{}, fn(arg_def, result) ->
{arg_def_name, arg_def_type} = arg_def
if value_ast = arg_ast_map[arg_def_name] do
Map.put result, arg_def_name, value_from_ast(value_ast, arg_def_type, variable_values)
else
result
end
end
end

defp value_from_ast(value_ast, _type, _variable_values) do
value_ast.value.value
end

defp field_entry_key(field) do
Map.get(field, :alias, field.name)
end
end
26 changes: 26 additions & 0 deletions lib/graphql/lang/lexer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule GraphQL.Lang.Lexer do
@moduledoc ~S"""
GraphQL lexer implemented with leex.
Tokenise a GraphQL query
iex> GraphQL.tokenize("{ hello }")
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
"""

@doc """
Tokenize the input string into a stream of tokens.
iex> GraphQL.tokenize("{ hello }")
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
"""
def tokenize(input_string) when is_binary(input_string) do
input_string |> to_char_list |> tokenize
end

def tokenize(input_string) do
{:ok, tokens, _} = :graphql_lexer.string input_string
tokens
end
end
53 changes: 53 additions & 0 deletions lib/graphql/lang/parser.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule GraphQL.Lang.Parser do
alias GraphQL.Lang.Lexer

@moduledoc ~S"""
GraphQL parser implemented with yecc.
Parse a GraphQL query
iex> GraphQL.parse "{ hello }"
{:ok, %{definitions: [
%{kind: :OperationDefinition, loc: %{start: 0},
operation: :query,
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
selections: [
%{kind: :Field, loc: %{start: 0}, name: "hello"}
]
}}
],
kind: :Document, loc: %{start: 0}
}}
"""

@doc """
Parse the input string into a Document AST.
iex> GraphQL.parse("{ hello }")
{:ok,
%{definitions: [
%{kind: :OperationDefinition, loc: %{start: 0},
operation: :query,
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
selections: [
%{kind: :Field, loc: %{start: 0}, name: "hello"}
]
}}
],
kind: :Document, loc: %{start: 0}
}
}
"""
def parse(input_string) when is_binary(input_string) do
input_string |> to_char_list |> parse
end

def parse(input_string) do
case input_string |> Lexer.tokenize |> :graphql_parser.parse do
{:ok, parse_result} ->
{:ok, parse_result}
{:error, {line_number, _, errors}} ->
{:error, %{errors: [%{message: "GraphQL: #{errors} on line #{line_number}", line_number: line_number}]}}
end
end
end
File renamed without changes.
Loading

0 comments on commit c9c6a59

Please sign in to comment.