184 lines
6.2 KiB
Python
184 lines
6.2 KiB
Python
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
|
|
|
|
from ..error import GraphQLError
|
|
from ..pyutils import (
|
|
Path,
|
|
did_you_mean,
|
|
inspect,
|
|
is_iterable,
|
|
print_path_list,
|
|
suggestion_list,
|
|
Undefined,
|
|
)
|
|
from ..type import (
|
|
GraphQLInputObjectType,
|
|
GraphQLInputType,
|
|
GraphQLList,
|
|
GraphQLScalarType,
|
|
is_leaf_type,
|
|
is_input_object_type,
|
|
is_list_type,
|
|
is_non_null_type,
|
|
GraphQLNonNull,
|
|
)
|
|
|
|
__all__ = ["coerce_input_value"]
|
|
|
|
|
|
OnErrorCB = Callable[[List[Union[str, int]], Any, GraphQLError], None]
|
|
|
|
|
|
def default_on_error(
|
|
path: List[Union[str, int]], invalid_value: Any, error: GraphQLError
|
|
) -> None:
|
|
error_prefix = "Invalid value " + inspect(invalid_value)
|
|
if path:
|
|
error_prefix += f" at 'value{print_path_list(path)}'"
|
|
error.message = error_prefix + ": " + error.message
|
|
raise error
|
|
|
|
|
|
def coerce_input_value(
|
|
input_value: Any,
|
|
type_: GraphQLInputType,
|
|
on_error: OnErrorCB = default_on_error,
|
|
path: Optional[Path] = None,
|
|
) -> Any:
|
|
"""Coerce a Python value given a GraphQL Input Type."""
|
|
if is_non_null_type(type_):
|
|
if input_value is not None and input_value is not Undefined:
|
|
type_ = cast(GraphQLNonNull, type_)
|
|
return coerce_input_value(input_value, type_.of_type, on_error, path)
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(
|
|
f"Expected non-nullable type '{inspect(type_)}' not to be None."
|
|
),
|
|
)
|
|
return Undefined
|
|
|
|
if input_value is None or input_value is Undefined:
|
|
# Explicitly return the value null.
|
|
return None
|
|
|
|
if is_list_type(type_):
|
|
type_ = cast(GraphQLList, type_)
|
|
item_type = type_.of_type
|
|
if is_iterable(input_value):
|
|
coerced_list: List[Any] = []
|
|
append_item = coerced_list.append
|
|
for index, item_value in enumerate(input_value):
|
|
append_item(
|
|
coerce_input_value(
|
|
item_value, item_type, on_error, Path(path, index, None)
|
|
)
|
|
)
|
|
return coerced_list
|
|
# Lists accept a non-list value as a list of one.
|
|
return [coerce_input_value(input_value, item_type, on_error, path)]
|
|
|
|
if is_input_object_type(type_):
|
|
type_ = cast(GraphQLInputObjectType, type_)
|
|
if not isinstance(input_value, dict):
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(f"Expected type '{type_.name}' to be a mapping."),
|
|
)
|
|
return Undefined
|
|
|
|
coerced_dict: Dict[str, Any] = {}
|
|
fields = type_.fields
|
|
|
|
for field_name, field in fields.items():
|
|
field_value = input_value.get(field_name, Undefined)
|
|
|
|
if field_value is Undefined:
|
|
if field.default_value is not Undefined:
|
|
# Use out name as name if it exists (extension of GraphQL.js).
|
|
coerced_dict[field.out_name or field_name] = field.default_value
|
|
elif is_non_null_type(field.type): # pragma: no cover else
|
|
type_str = inspect(field.type)
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(
|
|
f"Field '{field_name}' of required type '{type_str}'"
|
|
" was not provided."
|
|
),
|
|
)
|
|
continue
|
|
|
|
coerced_dict[field.out_name or field_name] = coerce_input_value(
|
|
field_value, field.type, on_error, Path(path, field_name, type_.name)
|
|
)
|
|
|
|
# Ensure every provided field is defined.
|
|
for field_name in input_value:
|
|
if field_name not in fields:
|
|
suggestions = suggestion_list(field_name, fields)
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(
|
|
f"Field '{field_name}' is not defined by type '{type_.name}'."
|
|
+ did_you_mean(suggestions)
|
|
),
|
|
)
|
|
|
|
if type_.is_one_of:
|
|
keys = list(coerced_dict)
|
|
if len(keys) != 1:
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(
|
|
"Exactly one key must be specified"
|
|
f" for OneOf type '{type_.name}'.",
|
|
),
|
|
)
|
|
else:
|
|
key = keys[0]
|
|
value = coerced_dict[key]
|
|
if value is None:
|
|
on_error(
|
|
(path.as_list() if path else []) + [key],
|
|
value,
|
|
GraphQLError(
|
|
f"Field '{key}' must be non-null.",
|
|
),
|
|
)
|
|
|
|
return type_.out_type(coerced_dict)
|
|
|
|
if is_leaf_type(type_):
|
|
# Scalars determine if a value is valid via `parse_value()`, which can throw to
|
|
# indicate failure. If it throws, maintain a reference to the original error.
|
|
type_ = cast(GraphQLScalarType, type_)
|
|
try:
|
|
parse_result = type_.parse_value(input_value)
|
|
except GraphQLError as error:
|
|
on_error(path.as_list() if path else [], input_value, error)
|
|
return Undefined
|
|
except Exception as error:
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(
|
|
f"Expected type '{type_.name}'. {error}", original_error=error
|
|
),
|
|
)
|
|
return Undefined
|
|
if parse_result is Undefined:
|
|
on_error(
|
|
path.as_list() if path else [],
|
|
input_value,
|
|
GraphQLError(f"Expected type '{type_.name}'."),
|
|
)
|
|
return parse_result
|
|
|
|
# Not reachable. All possible input types have been considered.
|
|
raise TypeError(f"Unexpected input type: {inspect(type_)}.")
|