Current section

Files

Jump to
absinthe lib absinthe execution arguments.ex
Raw

lib/absinthe/execution/arguments.ex

defmodule Absinthe.Execution.Arguments do
# Handles the logic around building and validating argument values for a field.
@moduledoc false
alias Absinthe.Validation
alias Absinthe.Execution
alias Absinthe.Type
alias Absinthe.Language
alias Absinthe.Schema
# Build an arguments map from the argument definitions in the schema, using the
# argument values from the query document.
@doc false
@spec build(Language.t | Language.t, %{atom => Type.Argument.t}, Execution.t) :: {:ok, {%{atom => any}, Execution.t}} | {:error, {[binary], [binary]}, Execution.t}
def build(ast_field, schema_arguments, execution) do
initial = {%{}, {[], []}, execution}
{values, {missing, invalid}, post_execution} = schema_arguments
|> Enum.reduce(initial, &(add_argument(&1, ast_field, &2)))
execution_to_return = report_extra_arguments(ast_field, schema_arguments |> Map.keys |> Enum.map(&to_string/1), post_execution)
case missing ++ invalid do
[] -> {:ok, values, execution_to_return}
_ -> {:error, {missing, invalid}, execution_to_return}
end
end
# Parse the argument value from the query document field
@spec add_argument({atom, Type.Argument.t}, Language.t, {map, {[binary], [binary]}, Execution.t}) :: {map, {[binary], [binary]}, Execution.t}
defp add_argument({name, definition}, ast_field, acc) do
ast_field
|> lookup_argument(name)
|> do_add_argument(definition, ast_field, acc)
end
# No argument found in the query document field
@spec do_add_argument(Language.Argument.t | nil, Type.Argument.t, Language.t, {map, {[binary], [binary]}, Execution.t}) :: {map, {[binary], [binary]}, Execution.t}
defp do_add_argument(nil, definition, ast_field, {values, {missing, invalid}, execution} = acc) do
cond do
Validation.RequiredInput.required?(definition) ->
internal_type = Schema.lookup_type(execution.schema, definition.type)
exe = execution
|> Execution.put_error(:argument, definition.name, &"Argument `#{&1}' (#{internal_type.name}): Not provided", at: ast_field)
{values, {[to_string(definition.name) | missing], invalid}, exe}
definition.default_value != nil ->
input_type = Schema.lookup_type(execution.schema, definition.type)
{
values |> Map.put(definition.name |> String.to_atom, definition.default_value),
{missing, invalid},
execution
}
true ->
acc
end
end
defp do_add_argument(ast_argument, definition, _ast_field, {values, {missing, invalid} = tracking, execution}) do
execution_with_deprecation = execution |> add_argument_deprecation(ast_argument.name, definition, ast_argument)
value_to_coerce = ast_argument.value || execution.variables[ast_argument.name] || definition.default_value
input_type = Schema.lookup_type(execution.schema, definition.type)
if input_type do
add_argument_value(input_type, value_to_coerce, ast_argument, [ast_argument.name], {values, tracking, execution_with_deprecation})
else
exe = execution
|> Execution.put_error(:argument, ast_argument.name, &"Argument `#{&1}' (#{definition.type |> Type.unwrap}): Unknown type", at: ast_argument)
{values, {missing, [ast_argument.name | invalid]}, exe}
end
end
defp add_argument_deprecation(execution, _name, %{deprecation: nil}, _ast_node) do
execution
end
defp add_argument_deprecation(execution, name, %{type: identifier, deprecation: %{reason: reason}}, ast_node) do
internal_type = Schema.lookup_type(execution.schema, identifier)
details = if reason, do: "; #{reason}", else: ""
execution
|> Execution.put_error(:argument, name, &"Argument `#{&1}' (#{internal_type.name}): Deprecated#{details}", at: ast_node)
end
# Coerce an input value into an input type, tracking errors
@spec add_argument_value(Type.input_t, any, Language.Argument.t, [binary], Execution.t) :: {any, Execution.t}
# Nil value
defp add_argument_value(input_type, nil, ast_argument, [value_name|_] = full_value_name, {values, {missing, invalid}, execution}) do
if Validation.RequiredInput.required?(input_type) do
name_to_report = full_value_name |> dotted_name
internal_type = Schema.lookup_type(execution.schema, input_type)
exe = execution
|> Execution.put_error(:argument, name_to_report, &"Argument `#{&1}' (#{internal_type.name}): Not provided", at: ast_argument)
{values, {[name_to_report | missing], invalid}, exe}
else
{
values |> Map.put(value_name |> String.to_existing_atom, nil),
{missing, invalid},
execution
}
end
end
# Non-nil value
defp add_argument_value(type, input_value, ast_argument, names, {_, _, execution} = acc) do
Schema.lookup_type(execution.schema, type)
|> do_add_argument_value(input_value, ast_argument, names, acc)
end
# Scalar value (inside a wrapping type) found
defp do_add_argument_value(%Type.Scalar{} = definition_type, %{value: internal_value}, ast_argument, names, acc) do
do_add_argument_value(definition_type, internal_value, ast_argument, names, acc)
end
# Variable value found
defp do_add_argument_value(definition_type, %Language.Variable{name: name}, ast_argument, names, {_, _, execution} = acc) do
add_argument_value(definition_type, execution.variables[name], ast_argument, names, acc)
end
# Wrapped scalar value found
defp do_add_argument_value(%Type.Scalar{} = type, %{value: internal_value}, ast_argument, names, acc) do
do_add_argument_value(type, internal_value, ast_argument, names, acc)
end
# Bare scalar value found
defp do_add_argument_value(%Type.Scalar{name: type_name, parse: parser}, internal_value, ast_argument, [value_name | _] = full_value_name, {values, {missing, invalid}, execution}) do
case parser.(internal_value) do
{:ok, coerced_value} ->
{values |> Map.put(value_name |> String.to_existing_atom, coerced_value), {missing, invalid}, execution}
:error ->
name_to_report = full_value_name |> dotted_name
{
values,
{missing, [name_to_report | invalid]},
execution |> Execution.put_error(:argument, name_to_report, &"Argument `#{&1}' (#{type_name}): Invalid value provided", at: ast_argument)
}
end
end
# Enum value found
defp do_add_argument_value(%Type.Enum{} = enum, %{value: raw_value}, ast_argument, [value_name | _] = full_value_name, {values, {missing, invalid}, execution}) do
case Type.Enum.get_value(enum, name: raw_value |> to_string) do
nil ->
name_to_report = full_value_name |> dotted_name
{
values,
{missing, [name_to_report | invalid]},
execution |> Execution.put_error(:argument, name_to_report, &"Argument `#{&1}' (Enum): Invalid value", at: ast_argument)
}
enum_value ->
execution_with_deprecation = execution |> add_enum_value_deprecation(ast_argument.name, enum, enum_value, ast_argument)
{values |> Map.put(value_name |> String.to_existing_atom, enum_value.value), {missing, invalid}, execution_with_deprecation}
end
end
# Input object value found
defp do_add_argument_value(%Type.InputObject{fields: schema_fields}, %{fields: input_fields}, ast_argument, [value_name | _] = names, {values, {missing, invalid}, execution}) do
{_, object_values, {new_missing, new_invalid}, execution_to_return} = schema_fields
|> Enum.reduce({names, %{}, {missing, invalid}, execution}, fn ({name, schema_field}, {acc_value_name, acc_values, {acc_missing, acc_invalid}, acc_execution}) ->
input_field = input_fields |> Enum.find(&(&1.name == name |> to_string))
full_value_name = [name |> to_string | acc_value_name]
case input_field do
nil ->
# No input value
if Validation.RequiredInput.required?(schema_field) do
name_to_report = full_value_name |> dotted_name
unwrapped_type = Schema.lookup_type(acc_execution.schema, schema_field.type)
{
acc_value_name,
acc_values,
{[name_to_report | acc_missing], invalid},
acc_execution
|> Execution.put_error(:argument, name_to_report, &"Argument `#{&1}' (#{unwrapped_type.name}): Not provided", at: ast_argument)
}
else
{acc_value_name, acc_values, {acc_missing, acc_invalid}, acc_execution}
end
%{value: value} ->
field_type = Schema.lookup_type(acc_execution.schema, schema_field.type)
{result_values, {result_missing, result_invalid}, next_execution} = add_argument_value(field_type, value, ast_argument, full_value_name, {acc_values, {acc_missing, acc_invalid}, acc_execution})
{
acc_value_name,
result_values,
{result_missing, result_invalid},
next_execution |> add_argument_deprecation(full_value_name |> dotted_name, schema_field, ast_argument)
}
end
end)
{
values |> Map.put(value_name |> String.to_existing_atom, object_values),
{new_missing, new_invalid},
execution_to_return
}
end
# TODO: When a definition can't be found, we should add some type of
# validation error
defp do_add_argument_value(nil, _value, _ast_argument, _names, acc) do
acc
end
defp add_enum_value_deprecation(execution, _name, _enum, %{deprecation: nil}, _ast_node) do
execution
end
defp add_enum_value_deprecation(execution, name, %{name: enum_type_name}, %{name: enum_value, deprecation: %{reason: reason}}, ast_node) do
details = if reason, do: "; #{reason}", else: ""
execution
|> Execution.put_error(:argument, name, &"Argument `#{&1}' (#{enum_type_name}): Enum value \"#{enum_value}\" deprecated#{details}", at: ast_node)
end
# Add errors for any additional arguments not present in the schema
@spec report_extra_arguments(Language.t, [binary], Execution.t) :: Execution.t
defp report_extra_arguments(ast_field, schema_argument_names, execution) do
ast_field.arguments
|> Enum.reduce(execution, fn ast_arg, acc ->
if Enum.member?(schema_argument_names, ast_arg.name) do
acc
else
execution
|> Execution.put_error(:argument, ast_arg.name, "Not present in schema", at: ast_arg)
end
end)
end
@spec lookup_argument(Language.t, atom) :: Language.Argument.t | nil
defp lookup_argument(ast_field, name) do
argument_name = name |> to_string
ast_field.arguments
|> Enum.find(&(&1.name == argument_name))
end
@spec dotted_name([binary]) :: binary
defp dotted_name(names) do
names |> Enum.reverse |> Enum.join(".")
end
end