From 01eadb522e78b0f246bbfccfbdd579b785029d1e Mon Sep 17 00:00:00 2001 From: Josh Price Date: Tue, 24 Nov 2015 18:09:12 +1100 Subject: [PATCH 01/10] Execute simple query following the ref implementation --- README.md | 6 +- lib/graphql.ex | 126 ++++++++++++++++++++++++++------- test/graphql_executor_test.exs | 73 +++++++++++++++---- 3 files changed, 159 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 272bc24..45580a6 100644 --- a/README.md +++ b/README.md @@ -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} ] } } diff --git a/lib/graphql.ex b/lib/graphql.ex index 10c63a3..59c376f 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -30,15 +30,12 @@ defmodule GraphQL do # {:ok, %{hello: "world"}} """ - alias GraphQL.Schema - alias GraphQL.SyntaxError - defmodule ObjectType do defstruct name: "RootQueryType", description: "", fields: [] end defmodule FieldDefinition do - defstruct name: nil, type: "String", resolve: nil + defstruct name: nil, type: "String", args: %{}, resolve: nil end @doc """ @@ -94,31 +91,106 @@ defmodule GraphQL do # iex> GraphQL.execute(schema, "{ hello }") # {:ok, %{hello: world}} """ - def execute(schema, query) do - case 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} + 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 = get_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 + hd(document.definitions) + end + + defp get_operation_root_type(schema, operation) do + schema.query + 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 - defp parse_argument(%{kind: :Argument, loc: _, name: name, value: %{kind: _, loc: _, value: value}}) do - {String.to_atom(name), value} + # 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, type, root_value, fields) do + {:error, "not implemented"} + 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 = field_def.resolve || fn(_, _, _) -> "default resolve" end + args = argument_values(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 + } + resolve_fn.(source, args, info) + end + + defp field_definition(schema, parent_type, field_name) do + Enum.find parent_type.fields, fn(field_def) -> field_def.name == field_name end + 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 + # field.alias || field.name + field.name end end diff --git a/test/graphql_executor_test.exs b/test/graphql_executor_test.exs index 836a0ea..e4b3473 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql_executor_test.exs @@ -2,8 +2,8 @@ defmodule GraphqlExecutorTest do use ExUnit.Case, async: true - def assert_execute(query, schema, data_store, expected_output) do - assert GraphQL.execute(query, schema, data_store) == expected_output + def assert_execute(query, schema, data, expected_output) do + assert GraphQL.execute(query, schema, data) == expected_output end defmodule TestSchema do @@ -15,37 +15,82 @@ defmodule GraphqlExecutorTest do %GraphQL.FieldDefinition{ name: "greeting", type: "String", - resolve: &greeting/1, + args: %{ + name: %{ type: "String" } + }, + resolve: &greeting/3, } ] } } end - def greeting(name: name), do: "Hello, #{name}!" - def greeting(_), do: greeting(name: "world") + def greeting(_, %{name: name}, _), do: "Hello, #{name}!" + def greeting(_, _, _), do: "Hello, world!" end test "basic query execution" do query = "{ greeting }" - assert GraphQL.execute(TestSchema.schema, query) == {:ok, %{greeting: "Hello, world!"}} + {:ok, doc} = GraphQL.parse query + assert GraphQL.execute(TestSchema.schema, doc) == {:ok, %{"greeting" => "Hello, world!"}} end test "query arguments" do query = "{ greeting(name: \"Elixir\") }" - assert GraphQL.execute(TestSchema.schema, query) == {:ok, %{greeting: "Hello, Elixir!"}} + {:ok, doc} = GraphQL.parse query + assert GraphQL.execute(TestSchema.schema, doc) == {:ok, %{"greeting" => "Hello, Elixir!"}} end - # test "simple selection set" do + # schema = %GraphQL.Schema{ + # query: %GraphQL.ObjectType{ + # name: "Q", + # fields: [ + # %GraphQL.FieldDefinition{name: "id", type: "Int", resolve: fn(p) -> p.id end}, + # %GraphQL.FieldDefinition{name: "name", type: "String", resolve: fn(p) -> p.name end}, + # %GraphQL.FieldDefinition{name: "age", type: "Int", resolve: fn(p) -> p.age end} + # ] + # } + # } # - # data_store = [ - # %Person{id: 0, name: 'Kate', age: '25'}, - # %Person{id: 1, name: 'Dave', age: '34'}, - # %Person{id: 2, name: 'Jeni', age: '45'} + # data = [ + # %{id: 0, name: 'Kate', age: 25}, + # %{id: 1, name: 'Dave', age: 34}, + # %{id: 2, name: 'Jeni', age: 45} # ] # - # assert_execute 'query dave { Person(id:1) { name } }', schema, data_store, - # ~S({"name": "Dave"}) + # {:ok, doc} = GraphQL.parse("query Q { Person(id:1) { name } }") + # assert GraphQL.execute(schema, doc, nil, nil, "Q") == {:ok, %{name: "Dave"}} # end + + # it('uses the query schema for queries', async () => { + # var doc = `query Q { a } mutation M { c } subscription S { a }`; + # var data = { a: 'b', c: 'd' }; + # var ast = parse(doc); + # var schema = new GraphQLSchema({ + # query: new GraphQLObjectType({ + # name: 'Q', + # fields: { + # a: { type: GraphQLString }, + # } + # }), + # mutation: new GraphQLObjectType({ + # name: 'M', + # fields: { + # c: { type: GraphQLString }, + # } + # }), + # subscription: new GraphQLObjectType({ + # name: 'S', + # fields: { + # a: { type: GraphQLString }, + # } + # }) + # }); + # + # var queryResult = await execute(schema, ast, data, {}, 'Q'); + # + # expect(queryResult).to.deep.equal({ data: { a: 'b' } }); + # }); + end From 77ddee6c268c32a406d01c4614c8df49accde991 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Fri, 27 Nov 2015 15:44:02 +1100 Subject: [PATCH 02/10] Fields defs are now maps not arrays --- lib/graphql.ex | 3 ++- test/graphql_executor_test.exs | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index 59c376f..caad7ea 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -168,7 +168,8 @@ defmodule GraphQL do end defp field_definition(schema, parent_type, field_name) do - Enum.find parent_type.fields, fn(field_def) -> field_def.name == field_name end + # TODO deal with introspection + parent_type.fields[String.to_atom field_name] end defp argument_values(arg_defs, arg_asts, variable_values) do diff --git a/test/graphql_executor_test.exs b/test/graphql_executor_test.exs index e4b3473..967927a 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql_executor_test.exs @@ -11,16 +11,15 @@ defmodule GraphqlExecutorTest do %GraphQL.Schema{ query: %GraphQL.ObjectType{ name: "RootQueryType", - fields: [ - %GraphQL.FieldDefinition{ - name: "greeting", + fields: %{ + greeting: %GraphQL.FieldDefinition{ type: "String", args: %{ name: %{ type: "String" } }, resolve: &greeting/3, } - ] + } } } end From a01fea2ee0f086c6233ec4ae7426533a1c12cb33 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Fri, 27 Nov 2015 17:03:24 +1100 Subject: [PATCH 03/10] Execute simple data fetching query --- lib/graphql.ex | 31 +++++++++++++++---- test/graphql_executor_test.exs | 55 +++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index caad7ea..3a9b042 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -31,7 +31,7 @@ defmodule GraphQL do """ defmodule ObjectType do - defstruct name: "RootQueryType", description: "", fields: [] + defstruct name: "RootQueryType", description: "", fields: [], args: %{} end defmodule FieldDefinition do @@ -135,7 +135,7 @@ defmodule GraphQL do end end - # source_value -> root_value + # 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) @@ -143,7 +143,7 @@ defmodule GraphQL do end defp execute_fields_serially(context, type, root_value, fields) do - {:error, "not implemented"} + {:error, "not yet implemented"} end defp resolve_field(context, parent_type, source, field_asts) do @@ -151,7 +151,7 @@ defmodule GraphQL do field_name = field_ast.name field_def = field_definition(context.schema, parent_type, field_name) return_type = field_def.type - resolve_fn = field_def.resolve || fn(_, _, _) -> "default resolve" end + resolve_fn = field_def.resolve || fn(_, _, _) -> "default resolve fn value" end args = argument_values(field_def.args, Map.get(field_ast, :arguments, %{}), context.variable_values) info = %{ field_name: field_name, @@ -164,7 +164,28 @@ defmodule GraphQL do operation: context.operation, variable_values: context.variable_values } - resolve_fn.(source, args, info) + result = resolve_fn.(source, args, info) + complete_value(context, return_type, field_asts, info, result) + 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 diff --git a/test/graphql_executor_test.exs b/test/graphql_executor_test.exs index 967927a..0860480 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql_executor_test.exs @@ -40,27 +40,40 @@ defmodule GraphqlExecutorTest do assert GraphQL.execute(TestSchema.schema, doc) == {:ok, %{"greeting" => "Hello, Elixir!"}} end - # test "simple selection set" do - # schema = %GraphQL.Schema{ - # query: %GraphQL.ObjectType{ - # name: "Q", - # fields: [ - # %GraphQL.FieldDefinition{name: "id", type: "Int", resolve: fn(p) -> p.id end}, - # %GraphQL.FieldDefinition{name: "name", type: "String", resolve: fn(p) -> p.name end}, - # %GraphQL.FieldDefinition{name: "age", type: "Int", resolve: fn(p) -> p.age end} - # ] - # } - # } - # - # data = [ - # %{id: 0, name: 'Kate', age: 25}, - # %{id: 1, name: 'Dave', age: 34}, - # %{id: 2, name: 'Jeni', age: 45} - # ] - # - # {:ok, doc} = GraphQL.parse("query Q { Person(id:1) { name } }") - # assert GraphQL.execute(schema, doc, nil, nil, "Q") == {:ok, %{name: "Dave"}} - # end + test "simple selection set" do + schema = %GraphQL.Schema{ + query: %GraphQL.ObjectType{ + name: "PersonQuery", + fields: %{ + person: %{ + type: %GraphQL.ObjectType{ + name: "Person", + fields: %{ + id: %GraphQL.FieldDefinition{name: "id", type: "String", resolve: fn(p, _, _) -> p.id end}, + name: %GraphQL.FieldDefinition{name: "name", type: "String", resolve: fn(p, _, _) -> p.name end}, + age: %GraphQL.FieldDefinition{name: "age", type: "Int", resolve: fn(p, _, _) -> p.age end} + } + }, + args: %{ + id: %{ type: "String" } + }, + resolve: fn(data, %{id: id}, _) -> + Enum.find data, fn(record) -> record.id == id end + end + } + } + } + } + + data = [ + %{id: "0", name: "Kate", age: 25}, + %{id: "1", name: "Dave", age: 34}, + %{id: "2", name: "Jeni", age: 45} + ] + + {:ok, doc} = GraphQL.parse ~S[{ person(id: "1") { name } }] + assert GraphQL.execute(schema, doc, data) == {:ok, %{"person" => %{"name" => "Dave"}}} + end # it('uses the query schema for queries', async () => { # var doc = `query Q { a } mutation M { c } subscription S { a }`; From 7eaf2bbb38a2dd3172eca9c824b4c9e309ebc2b6 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Fri, 27 Nov 2015 17:16:33 +1100 Subject: [PATCH 04/10] Add test for using named query operation --- lib/graphql.ex | 18 ++++++++----- test/graphql_executor_test.exs | 47 ++++++++++++---------------------- 2 files changed, 28 insertions(+), 37 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index 3a9b042..d237e9a 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -31,7 +31,7 @@ defmodule GraphQL do """ defmodule ObjectType do - defstruct name: "RootQueryType", description: "", fields: [], args: %{} + defstruct name: "RootQueryType", description: "", fields: [] end defmodule FieldDefinition do @@ -129,7 +129,7 @@ defmodule GraphQL do 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] + %{kind: :Field} -> Map.put(fields, field_entry_key(selection), [selection]) _ -> fields end end @@ -151,8 +151,9 @@ defmodule GraphQL do field_name = field_ast.name field_def = field_definition(context.schema, parent_type, field_name) return_type = field_def.type - resolve_fn = field_def.resolve || fn(_, _, _) -> "default resolve fn value" end - args = argument_values(field_def.args, Map.get(field_ast, :arguments, %{}), context.variable_values) + + 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, @@ -168,6 +169,10 @@ defmodule GraphQL do complete_value(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) @@ -195,7 +200,7 @@ defmodule GraphQL do 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 + 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 @@ -212,7 +217,6 @@ defmodule GraphQL do end defp field_entry_key(field) do - # field.alias || field.name - field.name + Map.get(field, :alias, field.name) end end diff --git a/test/graphql_executor_test.exs b/test/graphql_executor_test.exs index 0860480..f78cf3c 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql_executor_test.exs @@ -75,34 +75,21 @@ defmodule GraphqlExecutorTest do assert GraphQL.execute(schema, doc, data) == {:ok, %{"person" => %{"name" => "Dave"}}} end - # it('uses the query schema for queries', async () => { - # var doc = `query Q { a } mutation M { c } subscription S { a }`; - # var data = { a: 'b', c: 'd' }; - # var ast = parse(doc); - # var schema = new GraphQLSchema({ - # query: new GraphQLObjectType({ - # name: 'Q', - # fields: { - # a: { type: GraphQLString }, - # } - # }), - # mutation: new GraphQLObjectType({ - # name: 'M', - # fields: { - # c: { type: GraphQLString }, - # } - # }), - # subscription: new GraphQLObjectType({ - # name: 'S', - # fields: { - # a: { type: GraphQLString }, - # } - # }) - # }); - # - # var queryResult = await execute(schema, ast, data, {}, 'Q'); - # - # expect(queryResult).to.deep.equal({ data: { a: 'b' } }); - # }); - + test "use specified query operation" do + schema = %GraphQL.Schema{ + query: %GraphQL.ObjectType{ + name: "Q", + fields: %{a: %{ type: "String"}} + }, + mutation: %GraphQL.ObjectType{ + name: "M", + fields: %{b: %{ type: "String"}} + } + } + data = %{"a" => "A", "b" => "B"} + {:ok, doc} = GraphQL.parse "query Q { a } mutation M { c }" + assert GraphQL.execute(schema, doc, data, nil, "Q") == {:ok, + %{"a" => "A"} + } + end end From bce4eb0c22401cdd91add78117ca34105198bec6 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Sat, 28 Nov 2015 10:34:07 +1100 Subject: [PATCH 05/10] Handle named mutations --- lib/graphql.ex | 33 +++++++++++++++++++-------------- test/graphql_executor_test.exs | 20 +++++++++++++++++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index d237e9a..dd3e238 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -31,7 +31,7 @@ defmodule GraphQL do """ defmodule ObjectType do - defstruct name: "RootQueryType", description: "", fields: [] + defstruct name: "RootQueryType", description: "", fields: %{} end defmodule FieldDefinition do @@ -109,21 +109,25 @@ defmodule GraphQL do end defp execute_operation(context, operation, root_value) do - type = get_operation_root_type(context.schema, operation) + 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) + :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 - hd(document.definitions) + if operation_name do + Enum.find(document.definitions, fn(definition) -> definition.name == operation_name end) + else + hd(document.definitions) + end end - defp get_operation_root_type(schema, operation) do - schema.query + 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 @@ -142,8 +146,9 @@ defmodule GraphQL do end end - defp execute_fields_serially(context, type, root_value, fields) do - {:error, "not yet implemented"} + 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 @@ -166,17 +171,17 @@ defmodule GraphQL do variable_values: context.variable_values } result = resolve_fn.(source, args, info) - complete_value(context, return_type, field_asts, info, result) + 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_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) -> @@ -193,7 +198,7 @@ defmodule GraphQL do result end - defp field_definition(schema, parent_type, field_name) do + defp field_definition(_schema, parent_type, field_name) do # TODO deal with introspection parent_type.fields[String.to_atom field_name] end diff --git a/test/graphql_executor_test.exs b/test/graphql_executor_test.exs index f78cf3c..e677c0d 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql_executor_test.exs @@ -87,9 +87,23 @@ defmodule GraphqlExecutorTest do } } data = %{"a" => "A", "b" => "B"} - {:ok, doc} = GraphQL.parse "query Q { a } mutation M { c }" - assert GraphQL.execute(schema, doc, data, nil, "Q") == {:ok, - %{"a" => "A"} + {:ok, doc} = GraphQL.parse "query Q { a } mutation M { b }" + assert GraphQL.execute(schema, doc, data, nil, "Q") == {:ok, %{"a" => "A"}} + end + + test "use specified mutation operation" do + schema = %GraphQL.Schema{ + query: %GraphQL.ObjectType{ + name: "Q", + fields: %{a: %{ type: "String"}} + }, + mutation: %GraphQL.ObjectType{ + name: "M", + fields: %{b: %{ type: "String"}} + } } + data = %{"a" => "A", "b" => "B"} + {:ok, doc} = GraphQL.parse "query Q { a } mutation M { b }" + assert GraphQL.execute(schema, doc, data, nil, "M") == {:ok, %{"b" => "B"}} end end From a52a26b52a2949a7d4273f7908df2b3232c961e7 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Wed, 2 Dec 2015 08:44:14 +1100 Subject: [PATCH 06/10] Improve module structure, general tidy up --- lib/graphql.ex | 199 +----------------- lib/graphql/execution/executor.ex | 148 +++++++++++++ lib/graphql/lang/lexer.ex | 26 +++ lib/graphql/lang/parser.ex | 53 +++++ .../execution/executor_test.exs} | 32 +-- .../lang/lexer_test.exs} | 9 +- .../lang/parser_introspection_test.exs} | 0 .../lang/parser_kitchen_sink_test.exs} | 0 .../lang/parser_schema_kitchen_sink_test.exs} | 0 .../lang/parser_test.exs} | 0 test/graphql_test.exs | 30 +-- test/test_helper.exs | 11 +- 12 files changed, 258 insertions(+), 250 deletions(-) create mode 100644 lib/graphql/execution/executor.ex create mode 100644 lib/graphql/lang/lexer.ex create mode 100644 lib/graphql/lang/parser.ex rename test/{graphql_executor_test.exs => graphql/execution/executor_test.exs} (70%) rename test/{graphql_lexer_test.exs => graphql/lang/lexer_test.exs} (95%) rename test/{graphql_parser_introspection_test.exs => graphql/lang/parser_introspection_test.exs} (100%) rename test/{graphql_parser_kitchen_sink_test.exs => graphql/lang/parser_kitchen_sink_test.exs} (100%) rename test/{graphql_parser_schema_kitchen_sink_test.exs => graphql/lang/parser_schema_kitchen_sink_test.exs} (100%) rename test/{graphql_parser_test.exs => graphql/lang/parser_test.exs} (100%) diff --git a/lib/graphql.ex b/lib/graphql.ex index dd3e238..8eb00f7 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -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. @@ -38,190 +21,14 @@ defmodule GraphQL do defstruct name: nil, type: "String", args: %{}, 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 - end - @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) + def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do + {:ok, document} = GraphQL.Lang.Parser.parse(query) + GraphQL.Execution.Executor.execute(schema, document, root_value, variable_values, operation_name) end end diff --git a/lib/graphql/execution/executor.ex b/lib/graphql/execution/executor.ex new file mode 100644 index 0000000..8d3a361 --- /dev/null +++ b/lib/graphql/execution/executor.ex @@ -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 diff --git a/lib/graphql/lang/lexer.ex b/lib/graphql/lang/lexer.ex new file mode 100644 index 0000000..69ce7ef --- /dev/null +++ b/lib/graphql/lang/lexer.ex @@ -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 diff --git a/lib/graphql/lang/parser.ex b/lib/graphql/lang/parser.ex new file mode 100644 index 0000000..f828c61 --- /dev/null +++ b/lib/graphql/lang/parser.ex @@ -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 diff --git a/test/graphql_executor_test.exs b/test/graphql/execution/executor_test.exs similarity index 70% rename from test/graphql_executor_test.exs rename to test/graphql/execution/executor_test.exs index e677c0d..d2d93c6 100644 --- a/test/graphql_executor_test.exs +++ b/test/graphql/execution/executor_test.exs @@ -2,8 +2,17 @@ defmodule GraphqlExecutorTest do use ExUnit.Case, async: true - def assert_execute(query, schema, data, expected_output) do - assert GraphQL.execute(query, schema, data) == expected_output + alias GraphQL.Lang.Parser + alias GraphQL.Execution.Executor + + def assert_execute({query, schema}, expected_output) do + {:ok, doc} = Parser.parse(query) + assert Executor.execute(schema, doc) == {:ok, expected_output} + end + + def assert_execute({query, schema, data}, expected_output) do + {:ok, doc} = Parser.parse(query) + assert Executor.execute(schema, doc, data) == {:ok, expected_output} end defmodule TestSchema do @@ -29,15 +38,11 @@ defmodule GraphqlExecutorTest do end test "basic query execution" do - query = "{ greeting }" - {:ok, doc} = GraphQL.parse query - assert GraphQL.execute(TestSchema.schema, doc) == {:ok, %{"greeting" => "Hello, world!"}} + assert_execute {"{ greeting }", TestSchema.schema}, %{"greeting" => "Hello, world!"} end test "query arguments" do - query = "{ greeting(name: \"Elixir\") }" - {:ok, doc} = GraphQL.parse query - assert GraphQL.execute(TestSchema.schema, doc) == {:ok, %{"greeting" => "Hello, Elixir!"}} + assert_execute {~S[{ greeting(name: "Elixir") }], TestSchema.schema}, %{"greeting" => "Hello, Elixir!"} end test "simple selection set" do @@ -71,8 +76,7 @@ defmodule GraphqlExecutorTest do %{id: "2", name: "Jeni", age: 45} ] - {:ok, doc} = GraphQL.parse ~S[{ person(id: "1") { name } }] - assert GraphQL.execute(schema, doc, data) == {:ok, %{"person" => %{"name" => "Dave"}}} + assert_execute {~S[{ person(id: "1") { name } }], schema, data}, %{"person" => %{"name" => "Dave"}} end test "use specified query operation" do @@ -87,8 +91,8 @@ defmodule GraphqlExecutorTest do } } data = %{"a" => "A", "b" => "B"} - {:ok, doc} = GraphQL.parse "query Q { a } mutation M { b }" - assert GraphQL.execute(schema, doc, data, nil, "Q") == {:ok, %{"a" => "A"}} + {:ok, doc} = Parser.parse "query Q { a } mutation M { b }" + assert Executor.execute(schema, doc, data, nil, "Q") == {:ok, %{"a" => "A"}} end test "use specified mutation operation" do @@ -103,7 +107,7 @@ defmodule GraphqlExecutorTest do } } data = %{"a" => "A", "b" => "B"} - {:ok, doc} = GraphQL.parse "query Q { a } mutation M { b }" - assert GraphQL.execute(schema, doc, data, nil, "M") == {:ok, %{"b" => "B"}} + {:ok, doc} = Parser.parse "query Q { a } mutation M { b }" + assert Executor.execute(schema, doc, data, nil, "M") == {:ok, %{"b" => "B"}} end end diff --git a/test/graphql_lexer_test.exs b/test/graphql/lang/lexer_test.exs similarity index 95% rename from test/graphql_lexer_test.exs rename to test/graphql/lang/lexer_test.exs index c977aa2..3d8cc46 100644 --- a/test/graphql_lexer_test.exs +++ b/test/graphql/lang/lexer_test.exs @@ -1,7 +1,14 @@ defmodule GraphqlLexerTest do use ExUnit.Case, async: true - import ExUnit.TestHelpers + def assert_tokens(input, tokens) do + case :graphql_lexer.string(input) do + {:ok, output, _} -> + assert output == tokens + {:error, {_, :graphql_lexer, output}, _} -> + assert output == tokens + end + end # Ignored tokens test "WhiteSpace is ignored" do diff --git a/test/graphql_parser_introspection_test.exs b/test/graphql/lang/parser_introspection_test.exs similarity index 100% rename from test/graphql_parser_introspection_test.exs rename to test/graphql/lang/parser_introspection_test.exs diff --git a/test/graphql_parser_kitchen_sink_test.exs b/test/graphql/lang/parser_kitchen_sink_test.exs similarity index 100% rename from test/graphql_parser_kitchen_sink_test.exs rename to test/graphql/lang/parser_kitchen_sink_test.exs diff --git a/test/graphql_parser_schema_kitchen_sink_test.exs b/test/graphql/lang/parser_schema_kitchen_sink_test.exs similarity index 100% rename from test/graphql_parser_schema_kitchen_sink_test.exs rename to test/graphql/lang/parser_schema_kitchen_sink_test.exs diff --git a/test/graphql_parser_test.exs b/test/graphql/lang/parser_test.exs similarity index 100% rename from test/graphql_parser_test.exs rename to test/graphql/lang/parser_test.exs diff --git a/test/graphql_test.exs b/test/graphql_test.exs index a880e5d..dd8ad73 100644 --- a/test/graphql_test.exs +++ b/test/graphql_test.exs @@ -4,35 +4,7 @@ defmodule GraphQLTest do import ExUnit.TestHelpers - test "parse char list" do - assert_parse "{ hero }", - %{kind: :Document, - loc: %{start: 0}, - definitions: [%{kind: :OperationDefinition, - loc: %{start: 0}, - operation: :query, - selectionSet: %{kind: :SelectionSet, - loc: %{start: 0}, - selections: [%{kind: :Field, - loc: %{start: 0}, - name: "hero"}]}}]} - end - - test "parse string" do - assert_parse "{ hero }", - %{kind: :Document, - loc: %{start: 0}, - definitions: [%{kind: :OperationDefinition, - loc: %{start: 0}, - operation: :query, - selectionSet: %{kind: :SelectionSet, - loc: %{start: 0}, - selections: [%{kind: :Field, - loc: %{start: 0}, - name: "hero"}]}}]} - end - - test "ReportError with message" do + test "Report error with message" do assert_parse "a", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error assert_parse "a }", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error # assert_parse "", %{errors: [%{message: "GraphQL: syntax error before: on line 1", line_number: 1}]}, :error diff --git a/test/test_helper.exs b/test/test_helper.exs index d31d965..3c305f2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,16 +3,7 @@ ExUnit.start() defmodule ExUnit.TestHelpers do import ExUnit.Assertions - def assert_tokens(input, tokens) do - case :graphql_lexer.string(input) do - {:ok, output, _} -> - assert output == tokens - {:error, {_, :graphql_lexer, output}, _} -> - assert output == tokens - end - end - def assert_parse(input_string, expected_output, type \\ :ok) do - assert GraphQL.parse(input_string) == {type, expected_output} + assert GraphQL.Lang.Parser.parse(input_string) == {type, expected_output} end end From 9ed99142a90aea5ae5f8b94def7cb0e5628310d2 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Wed, 2 Dec 2015 11:18:42 +1100 Subject: [PATCH 07/10] Rename test modules to match new structure --- test/graphql/execution/executor_test.exs | 2 +- test/graphql/lang/lexer_test.exs | 2 +- test/graphql/lang/parser_introspection_test.exs | 2 +- test/graphql/lang/parser_kitchen_sink_test.exs | 2 +- test/graphql/lang/parser_schema_kitchen_sink_test.exs | 2 +- test/graphql/lang/parser_test.exs | 9 ++++++++- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/test/graphql/execution/executor_test.exs b/test/graphql/execution/executor_test.exs index d2d93c6..3be6463 100644 --- a/test/graphql/execution/executor_test.exs +++ b/test/graphql/execution/executor_test.exs @@ -1,5 +1,5 @@ -defmodule GraphqlExecutorTest do +defmodule GraphQL.Execution.Executor.ExecutorTest do use ExUnit.Case, async: true alias GraphQL.Lang.Parser diff --git a/test/graphql/lang/lexer_test.exs b/test/graphql/lang/lexer_test.exs index 3d8cc46..1ae709b 100644 --- a/test/graphql/lang/lexer_test.exs +++ b/test/graphql/lang/lexer_test.exs @@ -1,4 +1,4 @@ -defmodule GraphqlLexerTest do +defmodule GraphQL.Lang.Lexer.LexerTest do use ExUnit.Case, async: true def assert_tokens(input, tokens) do diff --git a/test/graphql/lang/parser_introspection_test.exs b/test/graphql/lang/parser_introspection_test.exs index 3293ba9..6cacc98 100644 --- a/test/graphql/lang/parser_introspection_test.exs +++ b/test/graphql/lang/parser_introspection_test.exs @@ -1,4 +1,4 @@ -defmodule GraphqlParserIntrospectionTest do +defmodule GraphQL.Lang.Parser.IntrospectionTest do use ExUnit.Case, async: true import ExUnit.TestHelpers diff --git a/test/graphql/lang/parser_kitchen_sink_test.exs b/test/graphql/lang/parser_kitchen_sink_test.exs index 603a6e8..781bbc5 100644 --- a/test/graphql/lang/parser_kitchen_sink_test.exs +++ b/test/graphql/lang/parser_kitchen_sink_test.exs @@ -1,4 +1,4 @@ -defmodule GraphqlParserKitchenSinkTest do +defmodule GraphQL.Lang.Parser.KitchenSinkTest do use ExUnit.Case, async: true import ExUnit.TestHelpers diff --git a/test/graphql/lang/parser_schema_kitchen_sink_test.exs b/test/graphql/lang/parser_schema_kitchen_sink_test.exs index 9320e02..5d70c78 100644 --- a/test/graphql/lang/parser_schema_kitchen_sink_test.exs +++ b/test/graphql/lang/parser_schema_kitchen_sink_test.exs @@ -1,4 +1,4 @@ -defmodule GraphqlParserSchemaKitchenSinkTest do +defmodule GraphQL.Lang.Parser.SchemaKitchenSinkTest do use ExUnit.Case, async: true import ExUnit.TestHelpers diff --git a/test/graphql/lang/parser_test.exs b/test/graphql/lang/parser_test.exs index d6b0086..43daa7e 100644 --- a/test/graphql/lang/parser_test.exs +++ b/test/graphql/lang/parser_test.exs @@ -1,8 +1,15 @@ -defmodule GraphqlParserTest do +defmodule GraphQL.Lang.Parser.ParserTest do use ExUnit.Case, async: true import ExUnit.TestHelpers + test "Report error with message" do + assert_parse "a", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error + assert_parse "a }", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error + # assert_parse "", %{errors: [%{message: "GraphQL: syntax error before: on line 1", line_number: 1}]}, :error + assert_parse "{}", %{errors: [%{message: "GraphQL: syntax error before: '}' on line 1", line_number: 1}]}, :error + end + test "simple selection set" do assert_parse "{ hero }", %{kind: :Document, From c0486a9729d278c19d443f611a1793a0cf1b98b6 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Wed, 2 Dec 2015 12:18:49 +1100 Subject: [PATCH 08/10] Handle parse errors from GraphQL.execute/5 --- lib/graphql.ex | 8 ++++++-- test/graphql_test.exs | 16 +++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/graphql.ex b/lib/graphql.ex index 8eb00f7..6378b97 100644 --- a/lib/graphql.ex +++ b/lib/graphql.ex @@ -28,7 +28,11 @@ defmodule GraphQL do # {:ok, %{hello: world}} """ def execute(schema, query, root_value \\ %{}, variable_values \\ %{}, operation_name \\ nil) do - {:ok, document} = GraphQL.Lang.Parser.parse(query) - GraphQL.Execution.Executor.execute(schema, document, root_value, variable_values, operation_name) + case GraphQL.Lang.Parser.parse(query) do + {:ok, document} -> + GraphQL.Execution.Executor.execute(schema, document, root_value, variable_values, operation_name) + {:error, errors} -> + {:error, errors} + end end end diff --git a/test/graphql_test.exs b/test/graphql_test.exs index dd8ad73..df1e4a7 100644 --- a/test/graphql_test.exs +++ b/test/graphql_test.exs @@ -4,10 +4,16 @@ defmodule GraphQLTest do import ExUnit.TestHelpers - test "Report error with message" do - assert_parse "a", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error - assert_parse "a }", %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}, :error - # assert_parse "", %{errors: [%{message: "GraphQL: syntax error before: on line 1", line_number: 1}]}, :error - assert_parse "{}", %{errors: [%{message: "GraphQL: syntax error before: '}' on line 1", line_number: 1}]}, :error + test "Execute simple query" do + schema = %GraphQL.Schema{query: %GraphQL.ObjectType{fields: %{a: %{type: "String"}}}} + assert GraphQL.execute(schema, "{ a }", %{"a" => "A"}) == {:ok, %{"a" => "A"}} + end + + test "Report parse error with message" do + schema = %GraphQL.Schema{query: %GraphQL.ObjectType{fields: %{a: %{type: "String"}}}} + assert GraphQL.execute(schema, "{") == + {:error, %{errors: [%{message: "GraphQL: syntax error before: on line 1", line_number: 1}]}} + assert GraphQL.execute(schema, "a") == + {:error, %{errors: [%{message: "GraphQL: syntax error before: \"a\" on line 1", line_number: 1}]}} end end From d3f2275c85d319c93959cc99242aff6195b8bca1 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Wed, 2 Dec 2015 12:23:24 +1100 Subject: [PATCH 09/10] Remove unused import --- test/graphql_test.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/graphql_test.exs b/test/graphql_test.exs index df1e4a7..e3fbeb3 100644 --- a/test/graphql_test.exs +++ b/test/graphql_test.exs @@ -2,8 +2,6 @@ defmodule GraphQLTest do use ExUnit.Case, async: true doctest GraphQL - import ExUnit.TestHelpers - test "Execute simple query" do schema = %GraphQL.Schema{query: %GraphQL.ObjectType{fields: %{a: %{type: "String"}}}} assert GraphQL.execute(schema, "{ a }", %{"a" => "A"}) == {:ok, %{"a" => "A"}} From e4d64b669d58a0ccdac01894bab4610a7792c1b0 Mon Sep 17 00:00:00 2001 From: Josh Price Date: Wed, 2 Dec 2015 12:44:13 +1100 Subject: [PATCH 10/10] Move Schema and SyntaxError to their own subfolders --- lib/graphql/{exceptions.ex => error/syntax_error.ex} | 0 lib/graphql/{ => type}/schema.ex | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/graphql/{exceptions.ex => error/syntax_error.ex} (100%) rename lib/graphql/{ => type}/schema.ex (100%) diff --git a/lib/graphql/exceptions.ex b/lib/graphql/error/syntax_error.ex similarity index 100% rename from lib/graphql/exceptions.ex rename to lib/graphql/error/syntax_error.ex diff --git a/lib/graphql/schema.ex b/lib/graphql/type/schema.ex similarity index 100% rename from lib/graphql/schema.ex rename to lib/graphql/type/schema.ex