use crate::executable::Field;
use crate::resolvers::execution::execute_selection_set;
use crate::resolvers::execution::try_nullify;
use crate::resolvers::execution::ExecutionContext;
use crate::resolvers::execution::ExecutionMode;
use crate::resolvers::execution::LinkedPath;
use crate::resolvers::execution::LinkedPathElement;
use crate::resolvers::execution::PropagateNull;
use crate::resolvers::AsyncObjectValue;
use crate::resolvers::AsyncResolvedValue;
use crate::resolvers::FieldError;
use crate::resolvers::MaybeAsync;
use crate::resolvers::MaybeAsyncResolved;
use crate::resolvers::ObjectValue;
use crate::resolvers::ResolvedValue;
use crate::response::GraphQLError;
use crate::response::JsonValue;
use crate::response::ResponseDataPathSegment;
use crate::schema::ExtendedType;
use crate::schema::Type;
use crate::validation::SuspectedValidationBug;
use futures::Stream;
use futures::StreamExt as _;
use std::pin::pin;
use std::pin::Pin;
enum LeafOrObject<'a> {
Leaf(JsonValue),
Object(MaybeAsync<Box<dyn AsyncObjectValue + 'a>, Box<dyn ObjectValue + 'a>>),
}
pub(crate) async fn complete_value<'a>(
ctx: &mut ExecutionContext<'a>,
path: LinkedPath<'_>,
mode: ExecutionMode,
ty: &'a Type,
resolved: MaybeAsyncResolved<'_>,
fields: &[&'a Field],
) -> Result<Option<JsonValue>, PropagateNull> {
let location = fields[0].name.location();
macro_rules! field_error {
($($arg: tt)+) => {
{
ctx.errors.push(GraphQLError::field_error(
format!($($arg)+),
path,
location,
&ctx.document.sources
));
return Err(PropagateNull);
}
};
}
let resolved = match resolved {
MaybeAsync::Async(AsyncResolvedValue::SkipForPartialExecution)
| MaybeAsync::Sync(ResolvedValue::SkipForPartialExecution) => return Ok(None),
MaybeAsync::Async(AsyncResolvedValue::Leaf(JsonValue::Null))
| MaybeAsync::Sync(ResolvedValue::Leaf(JsonValue::Null)) => {
if ty.is_non_null() {
field_error!("non-null type {ty} resolved to null")
} else {
return Ok(Some(JsonValue::Null));
}
}
MaybeAsync::Async(AsyncResolvedValue::List(stream)) => {
let stream = pin!(stream.map(|result| result.map(MaybeAsync::Async)));
return Box::pin(complete_list_value(ctx, path, mode, ty, fields, stream)).await;
}
MaybeAsync::Sync(ResolvedValue::List(iter)) => {
let stream = futures::stream::iter(iter);
let stream = pin!(stream.map(|result| result.map(MaybeAsync::Sync)));
return Box::pin(complete_list_value(ctx, path, mode, ty, fields, stream)).await;
}
MaybeAsync::Async(AsyncResolvedValue::Leaf(leaf))
| MaybeAsync::Sync(ResolvedValue::Leaf(leaf)) => LeafOrObject::Leaf(leaf),
MaybeAsync::Async(AsyncResolvedValue::Object(obj)) => {
LeafOrObject::Object(MaybeAsync::Async(obj))
}
MaybeAsync::Sync(ResolvedValue::Object(obj)) => LeafOrObject::Object(MaybeAsync::Sync(obj)),
};
let ty_name = match ty {
Type::List(_) | Type::NonNullList(_) => {
field_error!("list type {ty} resolved to an object")
}
Type::Named(name) | Type::NonNullNamed(name) => name,
};
let Some(ty_def) = ctx.schema.types.get(ty_name) else {
ctx.errors.push(
SuspectedValidationBug {
message: format!("undefined type {ty_name}"),
location,
}
.into_field_error(&ctx.document.sources, path),
);
return Err(PropagateNull);
};
if let ExtendedType::InputObject(_) = ty_def {
ctx.errors.push(
SuspectedValidationBug {
message: format!("field with input object type {ty_name}"),
location,
}
.into_field_error(&ctx.document.sources, path),
);
return Err(PropagateNull);
}
let resolved_obj = match resolved {
LeafOrObject::Leaf(json_value) => {
return complete_leaf_value(ctx, path, ty_name, ty_def, json_value, fields);
}
LeafOrObject::Object(resolved_obj) => resolved_obj,
};
let resolved_type_name = resolved_obj.type_name();
let object_type = match ty_def {
ExtendedType::InputObject(_) => unreachable!(), ExtendedType::Enum(_) | ExtendedType::Scalar(_) => {
field_error!(
"resolver returned a an object of type {resolved_type_name}, expected {ty_name}",
)
}
ExtendedType::Interface(_) | ExtendedType::Union(_) => {
let Some(object_def) = ctx.schema.get_object(resolved_type_name) else {
field_error!(
"resolver returned an object of type {resolved_type_name} \
not defined in the schema"
)
};
if let ExtendedType::Union(union_def) = ty_def {
if !union_def.members.contains(resolved_type_name) {
field_error!(
"resolver returned an object of type {resolved_type_name}, \
expected a member of union type {ty_name}"
)
}
} else if !object_def.implements_interfaces.contains(ty_name) {
field_error!(
"resolver returned an object of type {resolved_type_name} \
which does not implement interface {ty_name}"
)
}
object_def
}
ExtendedType::Object(def) => {
if resolved_type_name == ty_name.as_str() {
def
} else {
field_error!(
"resolver returned an object of type {resolved_type_name}, expected {ty_name}"
)
}
}
};
let resolved_obj = match &resolved_obj {
MaybeAsync::Async(obj) => MaybeAsync::Async(&**obj),
MaybeAsync::Sync(obj) => MaybeAsync::Sync(&**obj),
};
Box::pin(execute_selection_set(
ctx,
path,
mode,
object_type,
resolved_obj,
fields
.iter()
.flat_map(|field| &field.selection_set.selections),
))
.await
.map(|map| Some(JsonValue::Object(map)))
}
async fn complete_list_value<'a, 'b>(
ctx: &mut ExecutionContext<'a>,
path: LinkedPath<'_>,
mode: ExecutionMode,
ty: &'a Type,
fields: &[&'a Field],
stream: Pin<&mut dyn Stream<Item = Result<MaybeAsyncResolved<'b>, FieldError>>>,
) -> Result<Option<JsonValue>, PropagateNull> {
let inner_ty = match ty {
Type::Named(_) | Type::NonNullNamed(_) => {
let location = fields[0].name.location();
ctx.errors.push(GraphQLError::field_error(
format!("Non-list type {ty} resolved to a list"),
path,
location,
&ctx.document.sources,
));
return Err(PropagateNull);
}
Type::List(inner_ty) | Type::NonNullList(inner_ty) => inner_ty,
};
let mut completed_list = Vec::with_capacity(stream.size_hint().0);
let mut stream = stream.enumerate();
while let Some((index, inner_result)) = stream.next().await {
let inner_path = LinkedPathElement {
element: ResponseDataPathSegment::ListIndex(index),
next: path,
};
let inner_resolved = inner_result.map_err(|err| {
ctx.errors.push(GraphQLError::field_error(
format!("resolver error: {}", err.message),
Some(&inner_path),
fields[0].name.location(),
&ctx.document.sources,
));
PropagateNull
})?;
let inner_result = complete_value(
ctx,
Some(&inner_path),
mode,
inner_ty,
inner_resolved,
fields,
)
.await;
match try_nullify(inner_ty, inner_result) {
Ok(None) => {}
Ok(Some(inner_value)) => completed_list.push(inner_value),
Err(PropagateNull) => return try_nullify(ty, Err(PropagateNull)),
}
}
Ok(Some(completed_list.into()))
}
fn complete_leaf_value(
ctx: &mut ExecutionContext<'_>,
path: LinkedPath<'_>,
ty_name: &crate::Name,
ty_def: &ExtendedType,
json_value: JsonValue,
fields: &[&Field],
) -> Result<Option<JsonValue>, PropagateNull> {
let location = fields[0].name.location();
macro_rules! field_error {
($($arg: tt)+) => {
{
ctx.errors.push(GraphQLError::field_error(
format!($($arg)+),
path,
location,
&ctx.document.sources
));
return Err(PropagateNull);
}
};
}
match ty_def {
ExtendedType::InputObject(_) => unreachable!(), ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => {
field_error!("resolver returned a leaf value but expected an object for type {ty_name}")
}
ExtendedType::Enum(enum_def) => {
if !json_value
.as_str()
.is_some_and(|str| enum_def.values.contains_key(str))
{
field_error!("resolver returned {json_value}, expected enum {ty_name}")
}
}
ExtendedType::Scalar(_) => match ty_name.as_str() {
"Int" => {
if let Some(int) = json_value.as_i64() {
if i32::try_from(int).is_err() {
field_error!("resolver returned {json_value} which overflows Int")
}
} else {
field_error!("resolver returned {json_value}, expected Int")
}
}
"Float" => {
if !json_value.is_f64() {
field_error!("resolver returned {json_value}, expected Float")
}
}
"String" => {
if !json_value.is_string() {
field_error!("resolver returned {json_value}, expected String")
}
}
"Boolean" => {
if !json_value.is_boolean() {
field_error!("resolver returned {json_value}, expected Boolean")
}
}
"ID" => {
if !(json_value.is_string() || json_value.is_i64()) {
field_error!("resolver returned {json_value}, expected ID")
}
}
_ => {
}
},
};
Ok(Some(json_value))
}
#[test]
fn test_error_path() {
use crate::resolvers;
use crate::ExecutableDocument;
use crate::Schema;
let sdl = "type Query { f: [Int] }";
let query = "{ f }";
struct InitialValue;
impl resolvers::ObjectValue for InitialValue {
fn type_name(&self) -> &str {
"Query"
}
fn resolve_field<'a>(
&'a self,
info: &resolvers::ResolveInfo<'a>,
) -> Result<ResolvedValue<'a>, resolvers::FieldError> {
match info.field_name() {
"f" => Ok(ResolvedValue::List(Box::new(
[
Ok(ResolvedValue::leaf(42)),
Err(resolvers::FieldError {
message: "!".into(),
}),
]
.into_iter(),
))),
_ => Err(self.unknown_field_error(info)),
}
}
}
let schema = Schema::parse_and_validate(sdl, "schema.graphql").unwrap();
let document = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
let response = resolvers::Execution::new(&schema, &document)
.execute_sync(&InitialValue)
.unwrap();
let response = serde_json::to_string_pretty(&response).unwrap();
expect_test::expect![[r#"
{
"errors": [
{
"message": "resolver error: !",
"locations": [
{
"line": 1,
"column": 3
}
],
"path": [
"f",
1
]
}
],
"data": {
"f": null
}
}"#]]
.assert_eq(&response);
}