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

Commit c9c6a59

Browse files
author
Josh Price
committed
Merge pull request #25 from joshprice/execute-queries
Query execution in the style of reference implementation
2 parents 7a8a19b + e4d64b6 commit c9c6a59

16 files changed

+378
-198
lines changed

README.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@ defmodule TestSchema do
3939
query: %GraphQL.ObjectType{
4040
name: "RootQueryType",
4141
fields: [
42-
%GraphQL.FieldDefinition{
43-
name: "greeting",
44-
type: "String",
45-
resolve: &greeting/1,
46-
}
42+
%GraphQL.FieldDefinition{name: "greeting", type: "String", resolve: &greeting/1}
4743
]
4844
}
4945
}

lib/graphql.ex

+7-93
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,6 @@ defmodule GraphQL do
55
The `GraphQL` module provides a
66
[GraphQL](http://facebook.github.io/graphql/) implementation for Elixir.
77
8-
## Parse a query
9-
10-
Parse a GraphQL query
11-
12-
iex> GraphQL.parse "{ hello }"
13-
{:ok, %{definitions: [
14-
%{kind: :OperationDefinition, loc: %{start: 0},
15-
operation: :query,
16-
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
17-
selections: [
18-
%{kind: :Field, loc: %{start: 0}, name: "hello"}
19-
]
20-
}}
21-
],
22-
kind: :Document, loc: %{start: 0}
23-
}}
24-
258
## Execute a query
269
2710
Execute a GraphQL query against a given schema / datastore.
@@ -30,62 +13,12 @@ defmodule GraphQL do
3013
# {:ok, %{hello: "world"}}
3114
"""
3215

33-
alias GraphQL.Schema
34-
alias GraphQL.SyntaxError
35-
3616
defmodule ObjectType do
37-
defstruct name: "RootQueryType", description: "", fields: []
17+
defstruct name: "RootQueryType", description: "", fields: %{}
3818
end
3919

4020
defmodule FieldDefinition do
41-
defstruct name: nil, type: "String", resolve: nil
42-
end
43-
44-
@doc """
45-
Tokenize the input string into a stream of tokens.
46-
47-
iex> GraphQL.tokenize("{ hello }")
48-
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
49-
50-
"""
51-
def tokenize(input_string) when is_binary(input_string) do
52-
input_string |> to_char_list |> tokenize
53-
end
54-
55-
def tokenize(input_string) do
56-
{:ok, tokens, _} = :graphql_lexer.string input_string
57-
tokens
58-
end
59-
60-
@doc """
61-
Parse the input string into a Document AST.
62-
63-
iex> GraphQL.parse("{ hello }")
64-
{:ok,
65-
%{definitions: [
66-
%{kind: :OperationDefinition, loc: %{start: 0},
67-
operation: :query,
68-
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
69-
selections: [
70-
%{kind: :Field, loc: %{start: 0}, name: "hello"}
71-
]
72-
}}
73-
],
74-
kind: :Document, loc: %{start: 0}
75-
}
76-
}
77-
"""
78-
def parse(input_string) when is_binary(input_string) do
79-
input_string |> to_char_list |> parse
80-
end
81-
82-
def parse(input_string) do
83-
case input_string |> tokenize |> :graphql_parser.parse do
84-
{:ok, parse_result} ->
85-
{:ok, parse_result}
86-
{:error, {line_number, _, errors}} ->
87-
{:error, %{errors: [%{message: "GraphQL: #{errors} on line #{line_number}", line_number: line_number}]}}
88-
end
21+
defstruct name: nil, type: "String", args: %{}, resolve: nil
8922
end
9023

9124
@doc """
@@ -94,31 +27,12 @@ defmodule GraphQL do
9427
# iex> GraphQL.execute(schema, "{ hello }")
9528
# {:ok, %{hello: world}}
9629
"""
97-
def execute(schema, query) do
98-
case parse(query) do
30+
def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do
31+
case GraphQL.Lang.Parser.parse(query) do
9932
{:ok, document} ->
100-
query_fields = hd(document[:definitions])[:selectionSet][:selections]
101-
102-
%Schema{
103-
query: _query_root = %ObjectType{
104-
name: "RootQueryType",
105-
fields: fields
106-
}
107-
} = schema
108-
109-
result = for fd <- fields, qf <- query_fields, qf[:name] == fd.name do
110-
arguments = Map.get(qf, :arguments, [])
111-
|> Enum.map(&parse_argument/1)
112-
113-
{String.to_atom(fd.name), fd.resolve.(arguments)}
114-
end
115-
116-
{:ok, Enum.into(result, %{})}
117-
{:error, error} -> {:error, error}
33+
GraphQL.Execution.Executor.execute(schema, document, root_value, variable_values, operation_name)
34+
{:error, errors} ->
35+
{:error, errors}
11836
end
11937
end
120-
121-
defp parse_argument(%{kind: :Argument, loc: _, name: name, value: %{kind: _, loc: _, value: value}}) do
122-
{String.to_atom(name), value}
123-
end
12438
end
File renamed without changes.

lib/graphql/execution/executor.ex

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
defmodule GraphQL.Execution.Executor do
2+
@moduledoc ~S"""
3+
Execute a GraphQL query against a given schema / datastore.
4+
5+
# iex> GraphQL.execute schema, "{ hello }"
6+
# {:ok, %{hello: "world"}}
7+
"""
8+
9+
@doc """
10+
Execute a query against a schema.
11+
12+
# iex> GraphQL.execute(schema, "{ hello }")
13+
# {:ok, %{hello: world}}
14+
"""
15+
def execute(schema, document, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do
16+
context = build_execution_context(schema, document, root_value, variable_values, operation_name)
17+
{:ok, {data, _errors}} = execute_operation(context, context.operation, root_value)
18+
{:ok, data}
19+
end
20+
21+
defp build_execution_context(schema, document, root_value, variable_values, operation_name) do
22+
%{
23+
schema: schema,
24+
fragments: %{},
25+
root_value: root_value,
26+
operation: find_operation(document, operation_name),
27+
variable_values: variable_values,
28+
errors: []
29+
}
30+
end
31+
32+
defp execute_operation(context, operation, root_value) do
33+
type = operation_root_type(context.schema, operation)
34+
fields = collect_fields(context, type, operation.selectionSet)
35+
result = case operation.operation do
36+
:mutation -> execute_fields_serially(context, type, root_value, fields)
37+
_ -> execute_fields(context, type, root_value, fields)
38+
end
39+
{:ok, {result, nil}}
40+
end
41+
42+
defp find_operation(document, operation_name) do
43+
if operation_name do
44+
Enum.find(document.definitions, fn(definition) -> definition.name == operation_name end)
45+
else
46+
hd(document.definitions)
47+
end
48+
end
49+
50+
defp operation_root_type(schema, operation) do
51+
Map.get(schema, operation.operation)
52+
end
53+
54+
defp collect_fields(_context, _runtime_type, selection_set, fields \\ %{}, _visited_fragment_names \\ %{}) do
55+
Enum.reduce selection_set[:selections], fields, fn(selection, fields) ->
56+
case selection do
57+
%{kind: :Field} -> Map.put(fields, field_entry_key(selection), [selection])
58+
_ -> fields
59+
end
60+
end
61+
end
62+
63+
# source_value -> root_value?
64+
defp execute_fields(context, parent_type, source_value, fields) do
65+
Enum.reduce fields, %{}, fn({field_name, field_asts}, results) ->
66+
Map.put results, field_name, resolve_field(context, parent_type, source_value, field_asts)
67+
end
68+
end
69+
70+
defp execute_fields_serially(context, parent_type, source_value, fields) do
71+
# call execute_fields because no async operations yet
72+
execute_fields(context, parent_type, source_value, fields)
73+
end
74+
75+
defp resolve_field(context, parent_type, source, field_asts) do
76+
field_ast = hd(field_asts)
77+
field_name = field_ast.name
78+
field_def = field_definition(context.schema, parent_type, field_name)
79+
return_type = field_def.type
80+
81+
resolve_fn = Map.get(field_def, :resolve, &default_resolve_fn/3)
82+
args = argument_values(Map.get(field_def, :args, %{}), Map.get(field_ast, :arguments, %{}), context.variable_values)
83+
info = %{
84+
field_name: field_name,
85+
field_asts: field_asts,
86+
return_type: return_type,
87+
parent_type: parent_type,
88+
schema: context.schema,
89+
fragments: context.fragments,
90+
root_value: context.root_value,
91+
operation: context.operation,
92+
variable_values: context.variable_values
93+
}
94+
result = resolve_fn.(source, args, info)
95+
complete_value_catching_error(context, return_type, field_asts, info, result)
96+
end
97+
98+
defp default_resolve_fn(source, _args, %{field_name: field_name}) do
99+
source[field_name]
100+
end
101+
102+
defp complete_value_catching_error(context, return_type, field_asts, info, result) do
103+
# TODO lots of error checking
104+
complete_value(context, return_type, field_asts, info, result)
105+
end
106+
107+
defp complete_value(context, %GraphQL.ObjectType{} = return_type, field_asts, _info, result) do
108+
sub_field_asts = Enum.reduce field_asts, %{}, fn(field_ast, sub_field_asts) ->
109+
if selection_set = Map.get(field_ast, :selectionSet) do
110+
collect_fields(context, return_type, selection_set, sub_field_asts)
111+
else
112+
sub_field_asts
113+
end
114+
end
115+
execute_fields(context, return_type, result, sub_field_asts)
116+
end
117+
118+
defp complete_value(_context, _return_type, _field_asts, _info, result) do
119+
result
120+
end
121+
122+
defp field_definition(_schema, parent_type, field_name) do
123+
# TODO deal with introspection
124+
parent_type.fields[String.to_atom field_name]
125+
end
126+
127+
defp argument_values(arg_defs, arg_asts, variable_values) do
128+
arg_ast_map = Enum.reduce arg_asts, %{}, fn(arg_ast, result) ->
129+
Map.put(result, String.to_atom(arg_ast.name), arg_ast)
130+
end
131+
Enum.reduce arg_defs, %{}, fn(arg_def, result) ->
132+
{arg_def_name, arg_def_type} = arg_def
133+
if value_ast = arg_ast_map[arg_def_name] do
134+
Map.put result, arg_def_name, value_from_ast(value_ast, arg_def_type, variable_values)
135+
else
136+
result
137+
end
138+
end
139+
end
140+
141+
defp value_from_ast(value_ast, _type, _variable_values) do
142+
value_ast.value.value
143+
end
144+
145+
defp field_entry_key(field) do
146+
Map.get(field, :alias, field.name)
147+
end
148+
end

lib/graphql/lang/lexer.ex

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule GraphQL.Lang.Lexer do
2+
@moduledoc ~S"""
3+
GraphQL lexer implemented with leex.
4+
5+
Tokenise a GraphQL query
6+
7+
iex> GraphQL.tokenize("{ hello }")
8+
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
9+
"""
10+
11+
@doc """
12+
Tokenize the input string into a stream of tokens.
13+
14+
iex> GraphQL.tokenize("{ hello }")
15+
[{ :"{", 1 }, { :name, 1, 'hello' }, { :"}", 1 }]
16+
17+
"""
18+
def tokenize(input_string) when is_binary(input_string) do
19+
input_string |> to_char_list |> tokenize
20+
end
21+
22+
def tokenize(input_string) do
23+
{:ok, tokens, _} = :graphql_lexer.string input_string
24+
tokens
25+
end
26+
end

lib/graphql/lang/parser.ex

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule GraphQL.Lang.Parser do
2+
alias GraphQL.Lang.Lexer
3+
4+
@moduledoc ~S"""
5+
GraphQL parser implemented with yecc.
6+
7+
Parse a GraphQL query
8+
9+
iex> GraphQL.parse "{ hello }"
10+
{:ok, %{definitions: [
11+
%{kind: :OperationDefinition, loc: %{start: 0},
12+
operation: :query,
13+
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
14+
selections: [
15+
%{kind: :Field, loc: %{start: 0}, name: "hello"}
16+
]
17+
}}
18+
],
19+
kind: :Document, loc: %{start: 0}
20+
}}
21+
"""
22+
23+
@doc """
24+
Parse the input string into a Document AST.
25+
26+
iex> GraphQL.parse("{ hello }")
27+
{:ok,
28+
%{definitions: [
29+
%{kind: :OperationDefinition, loc: %{start: 0},
30+
operation: :query,
31+
selectionSet: %{kind: :SelectionSet, loc: %{start: 0},
32+
selections: [
33+
%{kind: :Field, loc: %{start: 0}, name: "hello"}
34+
]
35+
}}
36+
],
37+
kind: :Document, loc: %{start: 0}
38+
}
39+
}
40+
"""
41+
def parse(input_string) when is_binary(input_string) do
42+
input_string |> to_char_list |> parse
43+
end
44+
45+
def parse(input_string) do
46+
case input_string |> Lexer.tokenize |> :graphql_parser.parse do
47+
{:ok, parse_result} ->
48+
{:ok, parse_result}
49+
{:error, {line_number, _, errors}} ->
50+
{:error, %{errors: [%{message: "GraphQL: #{errors} on line #{line_number}", line_number: line_number}]}}
51+
end
52+
end
53+
end
File renamed without changes.

0 commit comments

Comments
 (0)