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

Commit f960a1e

Browse files
author
Josh Price
committed
Merge pull request #75 from freshtonic/validations/unique-operation-names
Validations/unique operation names
2 parents e92c3e6 + 2279a17 commit f960a1e

File tree

7 files changed

+512
-3
lines changed

7 files changed

+512
-3
lines changed
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
defmodule GraphQL.Lang.AST.ParallelVisitor do
3+
@moduledoc ~S"""
4+
A ParallelVisitor runs all child visitors in parallel instead of serially like the CompositeVisitor.
5+
6+
In this context, 'in parallel' really means that for each node in the AST, each visitor will be invoked
7+
for each node in the AST, but the :break/:continue return value of enter and leave is maintained per-visitor.
8+
9+
This means invividual visitors can bail out of AST processing as soon as possible and not waste cycles.
10+
11+
This code based on the graphql-js *visitInParallel* function.
12+
"""
13+
14+
alias GraphQL.Lang.AST.Visitor
15+
alias GraphQL.Lang.AST.InitialisingVisitor
16+
alias GraphQL.Lang.AST.PostprocessingVisitor
17+
18+
defstruct visitors: []
19+
20+
defimpl Visitor do
21+
def enter(visitor, node, accumulator) do
22+
accumulator = Enum.reduce(visitor.visitors, accumulator, fn(child_visitor, acc) ->
23+
if !skipping?(accumulator, child_visitor) do
24+
acc = case child_visitor |> Visitor.enter(node, accumulator) do
25+
{:continue, next_accumulator} -> next_accumulator
26+
{:break, next_accumulator} ->
27+
put_in(next_accumulator[:skipping][child_visitor], node)
28+
end
29+
end
30+
acc
31+
end)
32+
{:continue, accumulator}
33+
end
34+
35+
def leave(visitor, node, accumulator) do
36+
accumulator = Enum.reduce(visitor.visitors, accumulator, fn(child_visitor, acc) ->
37+
cond do
38+
!skipping?(acc, child_visitor) ->
39+
{_, next_accumulator} = child_visitor |> Visitor.leave(node, acc)
40+
next_accumulator
41+
accumulator[:skipping][child_visitor] == node ->
42+
put_in(acc[:skipping][child_visitor], false)
43+
true -> accumulator
44+
end
45+
end)
46+
{:continue, accumulator}
47+
end
48+
49+
defp skipping?(accumulator, child_visitor) do
50+
Map.has_key?(accumulator[:skipping], child_visitor)
51+
end
52+
end
53+
54+
defimpl InitialisingVisitor do
55+
def init(visitor, accumulator) do
56+
accumulator = put_in(accumulator[:skipping], %{})
57+
Enum.reduce(visitor.visitors, accumulator, &InitialisingVisitor.init/2)
58+
end
59+
end
60+
61+
defimpl PostprocessingVisitor do
62+
def finish(visitor, accumulator) do
63+
Enum.reduce(visitor.visitors, accumulator, &PostprocessingVisitor.finish/2)
64+
end
65+
end
66+
67+
end

lib/graphql/lang/ast/type_info.ex

+6-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ defmodule GraphQL.Lang.AST.TypeInfo do
5858
name == Introspection.meta(:typename)[:name] ->
5959
Introspection.meta(:typename)
6060
parent_type.__struct__ == GraphQL.Type.ObjectType || parent_type.__struct__ == GraphQL.Type.Interface ->
61-
parent_type.fields[name]
61+
# FIXME: this "function or map" logic is repeated in the executor. DRY IT UP.
62+
if is_function(parent_type.fields) do
63+
parent_type.fields.()[name]
64+
else
65+
parent_type.fields[name]
66+
end
6267
true ->
6368
nil
6469
end

lib/graphql/lang/ast/type_info_visitor.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule GraphQL.Lang.AST.TypeInfoVisitor do
1313
alias GraphQL.Util.Stack
1414
alias GraphQL.Lang.AST.TypeInfo
1515
alias GraphQL.Lang.AST.Visitor
16+
alias GraphQL.Schema
1617

1718
defstruct name: "TypeInfoVisitor"
1819

@@ -93,13 +94,13 @@ defmodule GraphQL.Lang.AST.TypeInfoVisitor do
9394
stack_push(:type_stack, type)
9495
kind when kind in [:InlineFragment, :FragmentDefinition] ->
9596
output_type = if node.typeCondition do
96-
Schema.type_from_ast(accumulator[:type_info].schema, node.typeCondition)
97+
Schema.type_from_ast(node.typeCondition, accumulator[:type_info].schema)
9798
else
9899
TypeInfo.type(accumulator[:type_info])
99100
end
100101
stack_push(:type_stack, output_type)
101102
:VariableDefinition ->
102-
input_type = Schema.type_from_ast(accumulator[:type_info].schema, node.type)
103+
input_type = Schema.type_from_ast(node.type, accumulator[:type_info].schema)
103104
stack_push(:input_type_stack, input_type)
104105
:Argument ->
105106
field_or_directive = TypeInfo.directive(accumulator[:type_info]) ||

lib/graphql/type/schema.ex

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule GraphQL.Schema do
1414

1515
defstruct query: nil, mutation: nil, types: []
1616

17+
# FIXME: I think *schema* should be the first argument in this module.
1718
def type_from_ast(nil, _), do: nil
1819
def type_from_ast(%{kind: :NonNullType,} = input_type_ast, schema) do
1920
%GraphQL.Type.NonNull{ofType: type_from_ast(input_type_ast.type, schema)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
defmodule GraphQL.Validation.Rules.UniqueOperationNames do
3+
4+
alias GraphQL.Lang.AST.Visitor
5+
alias GraphQL.Lang.AST.InitialisingVisitor
6+
7+
defstruct name: "UniqueOperationNames"
8+
9+
defimpl InitialisingVisitor do
10+
def init(_visitor, accumulator) do
11+
Map.merge(%{operation_names: %{}}, accumulator)
12+
end
13+
end
14+
15+
defimpl Visitor do
16+
def enter(_visitor, node, accumulator) do
17+
if node.kind == :OperationDefinition && Map.has_key?(node, :name) do
18+
op_name = node.name
19+
if op_name.value do
20+
if accumulator[:operation_names][op_name.value] do
21+
accumulator = put_in(
22+
accumulator[:validation_errors],
23+
[duplicate_operation_message(op_name)] ++ accumulator[:validation_errors]
24+
)
25+
else
26+
accumulator = put_in(accumulator[:operation_names][op_name.value], true)
27+
end
28+
end
29+
end
30+
{:continue, accumulator}
31+
end
32+
33+
def leave(_visitor, _node, accumulator) do
34+
{:continue, accumulator}
35+
end
36+
37+
defp duplicate_operation_message(op_name) do
38+
"There can only be one operation named '#{op_name.value}'."
39+
end
40+
end
41+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
Code.require_file "../../../support/validations.exs", __DIR__
3+
4+
defmodule GraphQL.Validation.Rules.UniqueOperationNamesTest do
5+
use ExUnit.Case, async: true
6+
7+
import ValidationsSupport
8+
9+
alias GraphQL.Validation.Rules.UniqueOperationNames
10+
11+
test "no operations" do
12+
assert_passes_rule("""
13+
fragment fragA on Type {
14+
field
15+
}
16+
""", %UniqueOperationNames{})
17+
end
18+
19+
test "one anon operation" do
20+
assert_passes_rule("""
21+
{
22+
field
23+
}
24+
""", %UniqueOperationNames{})
25+
end
26+
27+
test "one named operation" do
28+
assert_passes_rule("""
29+
query Foo {
30+
field
31+
}
32+
""", %UniqueOperationNames{})
33+
end
34+
35+
test "multiple operations" do
36+
assert_passes_rule("""
37+
query Foo {
38+
field
39+
}
40+
41+
query Bar {
42+
field
43+
}
44+
""", %UniqueOperationNames{})
45+
end
46+
47+
test "multiple operations of different types" do
48+
assert_passes_rule("""
49+
query Foo {
50+
field
51+
}
52+
53+
mutation Bar {
54+
field
55+
}
56+
57+
# TODO: add this when subscription support is added
58+
#subscription Baz {
59+
# field
60+
#}
61+
""", %UniqueOperationNames{})
62+
end
63+
64+
test "fragment and operation named the same" do
65+
assert_passes_rule("""
66+
query Foo {
67+
...Foo
68+
}
69+
fragment Foo on Type {
70+
field
71+
}
72+
""", %UniqueOperationNames{})
73+
end
74+
75+
test "multiple operations of same name" do
76+
assert_fails_rule("""
77+
query Foo {
78+
fieldA
79+
}
80+
query Foo {
81+
fieldB
82+
}
83+
""", %UniqueOperationNames{})
84+
end
85+
86+
test "multiple ops of same name of different types (mutation)" do
87+
assert_fails_rule("""
88+
query Foo {
89+
fieldA
90+
}
91+
mutation Foo {
92+
fieldB
93+
}
94+
""", %UniqueOperationNames{})
95+
end
96+
97+
@tag :skip
98+
test "multiple ops of same name of different types (subscription)" do
99+
assert_fails_rule("""
100+
query Foo {
101+
fieldA
102+
}
103+
subscription Foo {
104+
fieldB
105+
}
106+
""", %UniqueOperationNames{})
107+
end
108+
end

0 commit comments

Comments
 (0)