8000 Allow subtypes on dynamic schema by jeffutter · Pull Request #1697 · async-graphql/async-graphql · GitHub
[go: up one dir, main page]

Skip to content

Allow subtypes on dynamic schema #1697

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 83 additions & 45 deletions src/dynamic/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ impl SchemaInner {
let interface = ty.as_interface().ok_or_else(|| {
format!("Type \"{}\" is not interface", interface_name)
})?;
check_is_valid_implementation(obj, interface)?;
self.check_is_valid_implementation(obj, interface)?;
}
}
}
Expand Down Expand Up @@ -339,7 +339,7 @@ impl SchemaInner {
let implemenented_type = ty.as_interface().ok_or_else(|| {
format!("Type \"{}\" is not interface", interface_name)
})?;
check_is_valid_implementation(interface, implemenented_type)?;
self.check_is_valid_implementation(interface, implemenented_type)?;
}
}
}
Expand Down Expand Up @@ -373,69 +373,107 @@ impl SchemaInner {

Ok(())
}
}

fn check_is_valid_implementation(
implementing_type: &impl BaseContainer,
implemented_type: &Interface,
) -> Result<(), SchemaError> {
for field in implemented_type.fields.values() {
let impl_field = implementing_type.field(&field.name).ok_or_else(|| {
format!(
"{} \"{}\" requires field \"{}\" defined by interface \"{}\"",
implementing_type.graphql_type(),
implementing_type.name(),
field.name,
implemented_type.name
)
})?;

for arg in field.arguments.values() {
let impl_arg = match impl_field.argument(&arg.name) {
Some(impl_arg) => impl_arg,
None if !arg.ty.is_nullable() => {
return Err(format!(
fn check_is_child_type(&self, ty: &TypeRef, child: &TypeRef) -> bool {
if child.is_subtype(ty) {
return true;
}

fn rewrap_type(ty: &TypeRef, name: &str) -> TypeRef {
match ty {
TypeRef::Named(_cow) => TypeRef::named(name),
TypeRef::NonNull(type_ref) => {
TypeRef::NonNull(Box::new(rewrap_type(type_ref, name)))
}
TypeRef::List(type_ref) => TypeRef::List(Box::new(rewrap_type(type_ref, name))),
}
}

match self.types.get(child.type_name()) {
Some(Type::Object(object)) => object.implements.iter().any(|i_ty| {
if let Some(i_ty) = self.types.get(i_ty) {
let type_ref = rewrap_type(child, i_ty.name());

return self.check_is_child_type(ty, &type_ref);
}
false
}),
Some(Type::Interface(interface)) => interface.implements.iter().any(|i_ty| {
if let Some(i_ty) = self.types.get(i_ty) {
let type_ref = rewrap_type(child, i_ty.name());

return self.check_is_child_type(ty, &type_ref);
}
false
}),
_ => false,
}
}

fn check_is_valid_implementation(
&self,
implementing_type: &impl BaseContainer,
implemented_type: &Interface,
) -> Result<(), SchemaError> {
for field in implemented_type.fields.values() {
// Field on the implementing type
let impl_field = implementing_type.field(&field.name).ok_or_else(|| {
format!(
"{} \"{}\" requires field \"{}\" defined by interface \"{}\"",
implementing_type.graphql_type(),
implementing_type.name(),
field.name,
implemented_type.name
)
})?;

for arg in field.arguments.values() {
let impl_arg = match impl_field.argument(&arg.name) {
Some(impl_arg) => impl_arg,
None if !arg.ty.is_nullable() => {
return Err(format!(
"Field \"{}.{}\" requires argument \"{}\" defined by interface \"{}.{}\"",
implementing_type.name(),
field.name,
arg.name,
implemented_type.name,
field.name,
)
.into());
}
None => continue,
};

if !self.check_is_child_type(&arg.ty, &impl_arg.ty) {
return Err(format!(
"Argument \"{}.{}.{}\" is not sub-type of \"{}.{}.{}\"",
implemented_type.name,
field.name,
arg.name,
implementing_type.name(),
field.name,
arg.name
)
.into());
}
None => continue,
};
}

if !arg.ty.is_subtype(&impl_arg.ty) {
// field must return a type which is equal to or a sub-type of (covariant) the
// return type of implementedField field’s return type
if !self.check_is_child_type(&field.ty, &impl_field.ty()) {
return Err(format!(
"Argument \"{}.{}.{}\" is not sub-type of \"{}.{}.{}\"",
implemented_type.name,
field.name,
arg.name,
"Field \"{}.{}\" is not sub-type of \"{}.{}\"",
implementing_type.name(),
field.name,
arg.name
implemented_type.name,
field.name,
)
.into());
}
}

// field must return a type which is equal to or a sub-type of (covariant) the
// return type of implementedField field’s return type
if !impl_field.ty().is_subtype(&field.ty) {
return Err(format!(
"Field \"{}.{}\" is not sub-type of \"{}.{}\"",
implementing_type.name(),
field.name,
implemented_type.name,
field.name,
)
.into());
}
Ok(())
}

Ok(())
}

#[cfg(test)]
Expand Down
191 changes: 191 additions & 0 deletions src/dynamic/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ impl Interface {
#[cfg(test)]
mod tests {
use async_graphql_parser::Pos;
use indexmap::IndexMap;

use crate::{dynamic::*, value, PathSegment, ServerError, Value};

Expand Down Expand Up @@ -357,6 +358,196 @@ mod tests {
);
}

#[tokio::test]
async fn subtype_of_interface() {
let interface1 = Interface::new("MyInterface1")
.field(InterfaceField::new("a", TypeRef::named(TypeRef::INT)));

let obj_a = Object::new("MyObjA")
.implement("MyInterface1")
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(100))) })
}))
.field(Field::new("b", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(200))) })
}));

let obj_b = Object::new("MyObjB")
.implement("MyInterface1")
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(300))) })
}))
.field(Field::new("c", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(400))) })
}));

let interface2 = Interface::new("MyInterface2")
.field(InterfaceField::new("child", TypeRef::named("MyInterface1")));

let obj_c = Object::new("MyObjC")
.implement("MyInterface2")
.field(Field::new("child", TypeRef::named("MyObjA"), |_| {
FieldFuture::new(async {
Ok(Some(Value::Object(IndexMap::from([
(
async_graphql_value::Name::new("__typename"),
Value::from("MyObjC"),
),
(
async_graphql_value::Name::new("child"),
Value::Object(IndexMap::from([
(
async_graphql_value::Name::new("__typename"),
Value::from("MyObjA"),
),
(async_graphql_value::Name::new("a"), Value::from(100)),
(async_graphql_value::Name::new("b"), Value::from(200)),
])),
),
]))))
})
}))
.field(Field::new("d", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(400))) })
}));

let query = Object::new("Query").field(Field::new(
"valueC",
TypeRef::named_nn(obj_c.type_name()),
|_| FieldFuture::new(async { Ok(Some(FieldValue::NULL.with_type("MyObjC"))) }),
));

let schema = Schema::build(query.type_name(), None, None)
.register(obj_a)
.register(obj_b)
.register(obj_c)
.register(interface1)
.register(interface2)
.register(query)
.finish()
.unwrap();

let query = r#"
fragment A on MyObjA {
b
}

{
valueC { child { __typename a ...A } }
}
"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"valueC": {
"child": {
"__typename": "MyObjA",
"a": 100,
"b": 200,
},
}
})
);
}

#[tokio::test]
async fn subtype_of_interface_nn() {
let interface1 = Interface::new("MyInterface1")
.field(InterfaceField::new("a", TypeRef::named(TypeRef::INT)));

let obj_a = Object::new("MyObjA")
.implement("MyInterface1")
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(100))) })
}))
.field(Field::new("b", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(200))) })
}));

let obj_b = Object::new("MyObjB")
.implement("MyInterface1")
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(300))) })
}))
.field(Field::new("c", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(400))) })
}));

let interface2 = Interface::new("MyInterface2").field(InterfaceField::new(
"children",
TypeRef::named_nn_list("MyInterface1"),
));

let obj_c = Object::new("MyObjC")
.implement("MyInterface2")
.field(Field::new(
"children",
TypeRef::named_nn_list("MyObjA"),
|_| {
FieldFuture::new(async {
Ok(Some(vec![Value::Object(IndexMap::from([
(
async_graphql_value::Name::new("__typename"),
Value::from("MyObjC"),
),
(
async_graphql_value::Name::new("children"),
Value::List(vec![Value::Object(IndexMap::from([
(
async_graphql_value::Name::new("__typename"),
Value::from("MyObjA"),
),
(async_graphql_value::Name::new("a"), Value::from(100)),
(async_graphql_value::Name::new("b"), Value::from(200)),
]))]),
),
]))]))
})
},
))
.field(Field::new("d", TypeRef::named(TypeRef::INT), |_| {
FieldFuture::new(async { Ok(Some(Value::from(400))) })
}));

let query = Object::new("Query").field(Field::new(
"valueC",
TypeRef::named_nn(obj_c.type_name()),
|_| FieldFuture::new(async { Ok(Some::<Vec<Value>>(vec![])) }),
));

let schema = Schema::build(query.type_name(), None, None)
.register(obj_a)
.register(obj_b)
.register(obj_c)
.register(interface1)
.register(interface2)
.register(query)
.finish()
.unwrap();

let query = r#"
fragment A on MyObjA {
b
}

{
valueC { children { __typename a ...A } }
}
"#;
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
value!({
"valueC": {
"children": [{
"__typename": "MyObjA",
"a": 100,
"b": 200,
}],
}
})
);
}

#[tokio::test]
async fn does_not_implement() {
let obj_a = Object::new("MyObjA")
Expand Down
0