/
variable_types_match.ex
107 lines (91 loc) · 3.24 KB
/
variable_types_match.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
defmodule Absinthe.Phase.Document.Arguments.VariableTypesMatch do
# Partially implements: 5.8.5. All Variable Usages are Allowed
# Specifically, it implements "Variable usages must be compatible with the arguments they are passed to."
# See relevant counter-example: https://spec.graphql.org/draft/#example-2028e
use Absinthe.Phase
alias Absinthe.Blueprint
alias Absinthe.Blueprint.Document.{Operation, Fragment}
def run(blueprint, _) do
blueprint =
blueprint
|> check_operations()
|> check_fragments()
{:ok, blueprint}
end
def check_operations(%Blueprint{} = blueprint) do
blueprint
|> Map.update!(:operations, fn operations ->
Enum.map(operations, &check_variable_types/1)
end)
end
# A single fragment may be used by multiple operations.
# Each operation may define its own variables.
# This checks that each fragment is simultaneously consistent with the
# variables defined in each of the operations which use that fragment.
def check_fragments(%Blueprint{} = blueprint) do
blueprint
|> Map.update!(:fragments, fn fragments ->
fragments
|> Enum.map(fn fragment ->
blueprint.operations
|> Enum.filter(&Operation.uses?(&1, fragment))
|> Enum.reduce(fragment, fn operation, fragment_acc ->
check_variable_types(operation, fragment_acc)
end)
end)
end)
end
def check_variable_types(%Operation{} = op) do
variable_defs = Map.new(op.variable_definitions, &{&1.name, &1})
Blueprint.prewalk(op, &check_var_type(&1, op.name, variable_defs))
end
def check_variable_types(%Operation{} = op, %Fragment.Named{} = fragment) do
variable_defs = Map.new(op.variable_definitions, &{&1.name, &1})
Blueprint.prewalk(fragment, &check_var_type(&1, op.name, variable_defs))
end
defp check_var_type(%{schema_node: nil} = node, _, _) do
{:halt, node}
end
defp check_var_type(
%Blueprint.Input.Value{
raw: %{content: %Blueprint.Input.Variable{} = var},
schema_node: schema_node
} = node,
op_name,
variable_defs
) do
case Map.fetch(variable_defs, var.name) do
{:ok, %{schema_node: var_schema_type}} ->
# null vs not null is handled elsewhere
var_schema_type = Absinthe.Type.unwrap(var_schema_type)
arg_schema_type = Absinthe.Type.unwrap(schema_node)
if var_schema_type && arg_schema_type && var_schema_type.name != arg_schema_type.name do
# error
var_with_error =
put_error(var, %Absinthe.Phase.Error{
phase: __MODULE__,
message: error_message(op_name, var, var_schema_type.name, arg_schema_type.name),
locations: [var.source_location]
})
{:halt, put_in(node.raw.content, var_with_error)}
else
node
end
_ ->
node
end
end
defp check_var_type(node, _, _) do
node
end
def error_message(op, variable, var_type, arg_type) do
start =
case op || "" do
"" -> "Variable"
op -> "In operation `#{op}, variable"
end
"#{start} `#{Blueprint.Input.inspect(variable)}` of type `#{var_type}` found as input to argument of type `#{
arg_type
}`."
end
end