8000 Allow subtypes on dynamic schema · async-graphql/async-graphql@061b116 · GitHub
[go: up one dir, main page]

Skip to content

Commit 061b116

Browse files
committed
Allow subtypes on dynamic schema
This allows types that implement a parent type to have more specific types for fields present in the parent type.
1 parent 75a9d14 commit 061b116

File tree

2 files changed

+174
-45
lines changed

2 files changed

+174
-45
lines changed

src/dynamic/check.rs

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ impl SchemaInner {
177177
let interface = ty.as_interface().ok_or_else(|| {
178178
format!("Type \"{}\" is not interface", interface_name)
179179
})?;
180-
check_is_valid_implementation(obj, interface)?;
180+
self.check_is_valid_implementation(obj, interface)?;
181181
}
182182
}
183183
}
@@ -339,7 +339,7 @@ impl SchemaInner {
339339
let implemenented_type = ty.as_interface().ok_or_else(|| {
340340
format!("Type \"{}\" is not interface", interface_name)
341341
})?;
342-
check_is_valid_implementation(interface, implemenented_type)?;
342+
self.check_is_valid_implementation(interface, implemenented_type)?;
343343
}
344344
}
345345
}
@@ -373,69 +373,105 @@ impl SchemaInner {
373373

374374
Ok(())
375375
}
376-
}
377376

378-
fn check_is_valid_implementation(
379-
implementing_type: &impl BaseContainer,
380-
implemented_type: &Interface,
381-
) -> Result<(), SchemaError> {
382-
for field in implemented_type.fields.values() {
383-
let impl_field = implementing_type.field(&field.name).ok_or_else(|| {
384-
format!(
385-
"{} \"{}\" requires field \"{}\" defined by interface \"{}\"",
386-
implementing_type.graphql_type(),
387-
implementing_type.name(),
388-
field.name,
389-
implemented_type.name
390-
)
391-
})?;
392-
393-
for arg in field.arguments.values() {
394-
let impl_arg = match impl_field.argument(&arg.name) {
395-
Some(impl_arg) => impl_arg,
396-
None if !arg.ty.is_nullable() => {
397-
return Err(format!(
377+
fn check_is_child_type(&self, ty: &TypeRef, child: &TypeRef) -> bool {
378+
if child.is_subtype(ty) {
379+
return true;
380+
}
381+
382+
if let Some(child) = self.types.get(child.type_name()) {
383+
if let Some(child) = child.as_object() {
384+
return child.implements.iter().any(|i_ty| {
385+
if let Some(i_ty) = self.types.get(i_ty) {
386+
let type_ref = TypeRef::named(i_ty.name());
387+
388+
return self.check_is_child_type(ty, &type_ref);
389+
}
390+
false
391+
});
392+
}
393+
394+
if let Some(child) = child.as_interface() {
395+
return child.implements.iter().any(|i_ty| {
396+
if let Some(i_ty) = self.types.get(i_ty) {
397+
let type_ref = TypeRef::named(i_ty.name());
398+
399+
return self.check_is_child_type(ty, &type_ref);
400+
}
401+
false
402+
});
403+
}
404+
405+
return false;
406+
}
407+
408+
false
409+
}
410+
411+
fn check_is_valid_implementation(
412+
&self,
413+
implementing_type: &impl BaseContainer,
414+
implemented_type: &Interface,
415+
) -> Result<(), SchemaError> {
416+
for field in implemented_type.fields.values() {
417+
// Field on the implementing type
418+
let impl_field = implementing_type.field(&field.name).ok_or_else(|| {
419+
format!(
420+
"{} \"{}\" requires field \"{}\" defined by interface \"{}\"",
421+
implementing_type.graphql_type(),
422+
implementing_type.name(),
423+
field.name,
424+
implemented_type.name
425+
)
426+
})?;
427+
428+
for arg in field.arguments.values() {
429+
let impl_arg = match impl_field.argument(&arg.name) {
430+
Some(impl_arg) => impl_arg,
431+
None if !arg.ty.is_nullable() => {
432+
return Err(format!(
398433
"Field \"{}.{}\" requires argument \"{}\" defined by interface \"{}.{}\"",
399434
implementing_type.name(),
400435
field.name,
401436
arg.name,
402437
implemented_type.name,
403438
field.name,
439+
)
440+
.into());
441+
}
442+
None => continue,
443+
};
444+
445+
if !self.check_is_child_type(&arg.ty, &impl_arg.ty) {
446+
return Err(format!(
447 10000 +
"Argument \"{}.{}.{}\" is not sub-type of \"{}.{}.{}\"",
448+
implemented_type.name,
449+
field.name,
450+
arg.name,
451+
implementing_type.name(),
452+
field.name,
453+
arg.name
404454
)
405455
.into());
406456
}
407-
None => continue,
408-
};
457+
}
409458

410-
if !arg.ty.is_subtype(&impl_arg.ty) {
459+
// field must return a type which is equal to or a sub-type of (covariant) the
460+
// return type of implementedField field’s return type
461+
if !self.check_is_child_type(&field.ty, &impl_field.ty()) {
411462
return Err(format!(
412-
"Argument \"{}.{}.{}\" is not sub-type of \"{}.{}.{}\"",
413-
implemented_type.name,
414-
field.name,
415-
arg.name,
463+
"Field \"{}.{}\" is not sub-type of \"{}.{}\"",
416464
implementing_type.name(),
417465
field.name,
418-
arg.name
466+
implemented_type.name,
467+
field.name,
419468
)
420469
.into());
421470
}
422471
}
423472

424-
// field must return a type which is equal to or a sub-type of (covariant) the
425-
// return type of implementedField field’s return type
426-
if !impl_field.ty().is_subtype(&field.ty) {
427-
return Err(format!(
428-
"Field \"{}.{}\" is not sub-type of \"{}.{}\"",
429-
implementing_type.name(),
430-
field.name,
431-
implemented_type.name,
432-
field.name,
433-
)
434-
.into());
435-
}
473+
Ok(())
436474
}
437-
438-
Ok(())
439475
}
440476

441477
#[cfg(test)]

src/dynamic/interface.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ impl Interface {
280280
#[cfg(test)]
281281
mod tests {
282282
use async_graphql_parser::Pos;
283+
use indexmap::IndexMap;
283284

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

@@ -357,6 +358,98 @@ mod tests {
357358
);
358359
}
359360

361+
#[tokio::test]
362+
async fn subtype_of_interface() {
363+
let interface1 = Interface::new("MyInterface1")
364+
.field(InterfaceField::new("a", TypeRef::named(TypeRef::INT)));
365+
366+
let obj_a = Object::new("MyObjA")
367+
.implement("MyInterface1")
368+
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
369+
FieldFuture::new(async { Ok(Some(Value::from(100))) })
370+
}))
371+
.field(Field::new("b", TypeRef::named(TypeRef::INT), |_| {
372+
FieldFuture::new(async { Ok(Some(Value::from(200))) })
373+
}));
374+
375+
let obj_b = Object::new("MyObjB")
376+
.implement("MyInterface1")
377+
.field(Field::new("a", TypeRef::named(TypeRef::INT), |_| {
378+
FieldFuture::new(async { Ok(Some(Value::from(300))) })
379+
}))
380+
.field(Field::new("c", TypeRef::named(TypeRef::INT), |_| {
381+
FieldFuture::new(async { Ok(Some(Value::from(400))) })
382+
}));
383+
384+
let interface2 = Interface::new("MyInterface2")
385+
.field(InterfaceField::new("child", TypeRef::named("MyInterface1")));
386+
387+
let obj_c = Object::new("MyObjC")
388+
.implement("MyInterface2")
389+
.field(Field::new("child", TypeRef::named("MyObjA"), |_| {
390+
FieldFuture::new(async {
391+
Ok(Some(Value::Object(IndexMap::from([
392+
(
393+
async_graphql_value::Name::new("__typename"),
394+
Value::from("MyObjC"),
395+
),
396+
(
397+
async_graphql_value::Name::new("child"),
398+
Value::Object(IndexMap::from([
399+
(
400+
async_graphql_value::Name::new("__typename"),
401+
Value::from("MyObjA"),
402+
),
403+
(async_graphql_value::Name::new("a"), Value::from(100)),
404+
(async_graphql_value::Name::new("b"), Value::from(200)),
405+
])),
406+
),
407+
]))))
408+
})
409+
}))
410+
.field(Field::new("d", TypeRef::named(TypeRef::INT), |_| {
411+
FieldFuture::new(async { Ok(Some(Value::from(400))) })
412+
}));
413+
414+
let query = Object::new("Query").field(Field::new(
415+
"valueC",
416+
TypeRef::named_nn(obj_c.type_name()),
417+
|_| FieldFuture::new(async { Ok(Some(FieldValue::NULL.with_type("MyObjC"))) }),
418+
));
419+
420+
let schema = Schema::build(query.type_name(), None, None)
421+
.register(obj_a)
422+
.register(obj_b)
423+
.register(obj_c)
424+
.register(interface1)
425+
.register(interface2)
426+
.register(query)
427+
.finish()
428+
.unwrap();
429+
430+
let query = r#"
431+
fragment A on MyObjA {
432+
b
433+
}
434+
435+
{
436+
valueC { child { __typename a ...A } }
437+
}
438+
"#;
439+
assert_eq!(
440+
schema.execute(query).await.into_result().unwrap().data,
441+
value!({
442+
"valueC": {
443+
"child": {
444+
"__typename": "MyObjA",
445+
"a": 100,
446+
"b": 200,
447+
},
448+
}
449+
})
450+
);
451+
}
452+
360453
#[tokio::test]
361454
async fn does_not_implement() {
362455
let obj_a = Object::new("MyObjA")

0 commit comments

Comments
 (0)
0