Allow interfaces to implement other interfaces (#1028, #1000)

This commit is contained in:
ilslv 2022-06-27 15:09:44 +03:00 committed by GitHub
parent bd041222df
commit 9ca2364bfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2440 additions and 61 deletions

View file

@ -77,6 +77,190 @@ struct Human {
```
### Interfaces implementing other interfaces
GraphQL allows implementing interfaces on other interfaces in addition to objects.
```rust
# extern crate juniper;
use juniper::{graphql_interface, graphql_object, ID};
#[graphql_interface(for = [HumanValue, Luke])]
struct Node {
id: ID,
}
#[graphql_interface(impl = NodeValue, for = Luke)]
struct Human {
id: ID,
home_planet: String,
}
struct Luke {
id: ID,
}
#[graphql_object(impl = [HumanValue, NodeValue])]
impl Luke {
fn id(&self) -> &ID {
&self.id
}
// As `String` and `&str` aren't distinguished by
// GraphQL spec, you can use them interchangeably.
// Same is applied for `Cow<'a, str>`.
// ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
fn home_planet() -> &'static str {
"Tatooine"
}
}
#
# fn main() {}
```
> __NOTE:__ Every interface has to specify all other interfaces/objects it implements or implemented for. Missing one of `for = ` or `impl = ` attributes is a compile-time error.
```compile_fail
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
pub struct ObjA {
id: String,
}
#[graphql_interface(for = ObjA)]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at
// 'Failed to implement interface `Character` on `ObjA`: missing interface reference in implementer's `impl` attribute.'
struct Character {
id: String,
}
fn main() {}
```
### GraphQL subtyping and additional `null`able fields
GraphQL allows implementers (both objects and other interfaces) to return "subtypes" instead of an original value. Basically, this allows you to impose additional bounds on the implementation.
Valid "subtypes" are:
- interface implementer instead of an interface itself:
- `I implements T` in place of a `T`;
- `Vec<I implements T>` in place of a `Vec<T>`.
- non-null value in place of a nullable:
- `T` in place of a `Option<T>`;
- `Vec<T>` in place of a `Vec<Option<T>>`.
These rules are recursively applied, so `Vec<Vec<I implements T>>` is a valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
Also, GraphQL allows implementers to add `null`able fields, which aren't present on an original interface.
```rust
# extern crate juniper;
use juniper::{graphql_interface, graphql_object, ID};
#[graphql_interface(for = [HumanValue, Luke])]
struct Node {
id: ID,
}
#[graphql_interface(for = HumanConnectionValue)]
struct Connection {
nodes: Vec<NodeValue>,
}
#[graphql_interface(impl = NodeValue, for = Luke)]
struct Human {
id: ID,
home_planet: String,
}
#[graphql_interface(impl = ConnectionValue)]
struct HumanConnection {
nodes: Vec<HumanValue>,
// ^^^^^^^^^^ notice not `NodeValue`
// This can happen, because every `Human` is a `Node` too, so we are just
// imposing additional bounds, which still can be resolved with
// `... on Connection { nodes }`.
}
struct Luke {
id: ID,
}
#[graphql_object(impl = [HumanValue, NodeValue])]
impl Luke {
fn id(&self) -> &ID {
&self.id
}
fn home_planet(language: Option<String>) -> &'static str {
// ^^^^^^^^^^^^^^
// Notice additional `null`able field, which is missing on `Human`.
// Resolving `...on Human { homePlanet }` will provide `None` for this
// argument.
match language.as_deref() {
None | Some("en") => "Tatooine",
Some("ko") => "타투인",
_ => todo!(),
}
}
}
#
# fn main() {}
```
Violating GraphQL "subtyping" or additional nullable field rules is a compile-time error.
```compile_fail
# extern crate juniper;
use juniper::{graphql_interface, graphql_object};
pub struct ObjA {
id: String,
}
#[graphql_object(impl = CharacterValue)]
impl ObjA {
fn id(&self, is_present: bool) -> &str {
// ^^ the evaluated program panicked at
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument `isPresent` of type `Boolean!`
// isn't present on the interface and so has to be nullable.'
is_present.then(|| self.id.as_str()).unwrap_or("missing")
}
}
#[graphql_interface(for = ObjA)]
struct Character {
id: String,
}
#
# fn main() {}
```
```compile_fail
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
id: Vec<String>,
// ^^ the evaluated program panicked at
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: implementor is expected to return a subtype of
// interface's return object: `[String!]!` is not a subtype of `String!`.'
}
#[graphql_interface(for = ObjA)]
struct Character {
id: String,
}
#
# fn main() {}
```
### Ignoring trait methods
We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them.

View file

@ -31,6 +31,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Forbade default implementations of non-ignored trait methods.
- Supported coercion of additional `null`able arguments and return sub-typing on implementer.
- Supported `rename_all = "<policy>"` attribute argument influencing all its fields and their arguments. ([#971])
- Supported interfaces implementing other interfaces. ([#1028])
- Split `#[derive(GraphQLScalarValue)]` macro into:
- `#[derive(GraphQLScalar)]` for implementing GraphQL scalar: ([#1017])
- Supported generic `ScalarValue`.
@ -94,6 +95,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1017]: /../../pull/1017
[#1025]: /../../pull/1025
[#1026]: /../../pull/1026
[#1028]: /../../pull/1028
[#1051]: /../../issues/1051
[#1054]: /../../pull/1054
[#1057]: /../../pull/1057

View file

@ -247,7 +247,7 @@ async fn interface_introspection() {
);
assert_eq!(
type_info.get_field_value("interfaces"),
Some(&graphql_value!(null)),
Some(&graphql_value!([])),
);
assert_eq!(
type_info.get_field_value("enumValues"),

View file

@ -551,6 +551,38 @@ macro_rules! assert_interfaces_impls {
};
}
/// Asserts that all [transitive interfaces][0] (the ones implemented by the
/// `$interface`) are also implemented by the `$implementor`.
///
/// [0]: https://spec.graphql.org/October2021#sel-FAHbhBHCAACGB35P
#[macro_export]
macro_rules! assert_transitive_impls {
($scalar: ty, $interface: ty, $implementor: ty $(, $transitive: ty)* $(,)?) => {
const _: () = {
$({
let is_present = $crate::macros::reflect::str_exists_in_arr(
<$implementor as ::juniper::macros::reflect::BaseType<$scalar>>::NAME,
<$transitive as ::juniper::macros::reflect::BaseSubTypes<$scalar>>::NAMES,
);
if !is_present {
const MSG: &str = $crate::const_concat!(
"Failed to implement interface `",
<$interface as $crate::macros::reflect::BaseType<$scalar>>::NAME,
"` on `",
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
"`: missing `impl = ` for transitive interface `",
<$transitive as $crate::macros::reflect::BaseType<$scalar>>::NAME,
"` on `",
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
"`."
);
::std::panic!("{}", MSG);
}
})*
};
};
}
/// Asserts validness of [`Field`] [`Arguments`] and returned [`Type`].
///
/// This assertion is a combination of [`assert_subtype`] and

View file

@ -110,6 +110,8 @@ pub struct InterfaceMeta<'a, S> {
pub description: Option<String>,
#[doc(hidden)]
pub fields: Vec<Field<'a, S>>,
#[doc(hidden)]
pub interface_names: Vec<String>,
}
/// Union type metadata
@ -587,6 +589,7 @@ impl<'a, S> InterfaceMeta<'a, S> {
name,
description: None,
fields: fields.to_vec(),
interface_names: Vec::new(),
}
}
@ -599,6 +602,18 @@ impl<'a, S> InterfaceMeta<'a, S> {
self
}
/// Sets the `interfaces` this [`InterfaceMeta`] interface implements.
///
/// Overwrites any previously set list of interfaces.
#[must_use]
pub fn interfaces(mut self, interfaces: &[Type<'a>]) -> Self {
self.interface_names = interfaces
.iter()
.map(|t| t.innermost_name().to_owned())
.collect();
self
}
/// Wraps this [`InterfaceMeta`] type into a generic [`MetaType`].
pub fn into_meta(self) -> MetaType<'a, S> {
MetaType::Interface(self)

View file

@ -244,10 +244,16 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
fn interfaces<'s>(&self, context: &'s SchemaType<'a, S>) -> Option<Vec<TypeType<'s, S>>> {
match self {
TypeType::Concrete(&MetaType::Object(ObjectMeta {
TypeType::Concrete(
&MetaType::Object(ObjectMeta {
ref interface_names,
..
})) => Some(
})
| &MetaType::Interface(InterfaceMeta {
ref interface_names,
..
}),
) => Some(
interface_names
.iter()
.filter_map(|n| context.type_by_name(n))

View file

@ -190,8 +190,11 @@ impl GraphQLParserTranslator {
position: Pos::default(),
description: x.description.as_ref().map(|s| From::from(s.as_str())),
name: From::from(x.name.as_ref()),
// TODO: Support this with GraphQL October 2021 Edition.
implements_interfaces: vec![],
implements_interfaces: x
.interface_names
.iter()
.map(|s| From::from(s.as_str()))
.collect(),
directives: vec![],
fields: x
.fields

View file

@ -1173,7 +1173,7 @@ pub(crate) fn schema_introspection_result() -> Value {
}
],
"inputFields": null,
"interfaces": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": [
{
@ -2500,7 +2500,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
}
],
"inputFields": null,
"interfaces": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": [
{

View file

@ -2,6 +2,7 @@ use std::fmt::Debug;
use crate::{
ast::{Definition, Document, FragmentSpread, InlineFragment},
meta::InterfaceMeta,
parser::Spanning,
schema::meta::MetaType,
validation::{ValidatorContext, Visitor},
@ -45,6 +46,23 @@ where
.as_ref()
.and_then(|s| ctx.schema.concrete_type_by_name(s.item)),
) {
// Even if there is no object type in the overlap of interfaces
// implementers, it's OK to spread in case `frag_type` implements
// `parent_type`.
// https://spec.graphql.org/October2021#sel-JALVFJNRDABABqDy5B
if let MetaType::Interface(InterfaceMeta {
interface_names, ..
}) = frag_type
{
let implements_parent = parent_type
.name()
.map(|parent| interface_names.iter().any(|i| i == parent))
.unwrap_or_default();
if implements_parent {
return;
}
}
if !ctx.schema.type_overlap(parent_type, frag_type) {
ctx.report_error(
&error_message(
@ -67,6 +85,23 @@ where
ctx.parent_type(),
self.fragment_types.get(spread.item.name.item),
) {
// Even if there is no object type in the overlap of interfaces
// implementers, it's OK to spread in case `frag_type` implements
// `parent_type`.
// https://spec.graphql.org/October2021/#sel-JALVFJNRDABABqDy5B
if let MetaType::Interface(InterfaceMeta {
interface_names, ..
}) = frag_type
{
let implements_parent = parent_type
.name()
.map(|parent| interface_names.iter().any(|i| i == parent))
.unwrap_or_default();
if implements_parent {
return;
}
}
if !ctx.schema.type_overlap(parent_type, frag_type) {
ctx.report_error(
&error_message(
@ -226,6 +261,27 @@ mod tests {
);
}
#[test]
fn no_object_overlap_but_implements_parent() {
expect_passes_rule::<_, _, DefaultScalarValue>(
factory,
r#"
fragment beingFragment on Being { ...unpopulatedFragment }
fragment unpopulatedFragment on Unpopulated { name }
"#,
);
}
#[test]
fn no_object_overlap_but_implements_parent_inline() {
expect_passes_rule::<_, _, DefaultScalarValue>(
factory,
r#"
fragment beingFragment on Being { ...on Unpopulated { name } }
"#,
);
}
#[test]
fn different_object_into_object() {
expect_fails_rule::<_, _, DefaultScalarValue>(

View file

@ -20,6 +20,7 @@ use crate::{
struct Being;
struct Pet;
struct Canine;
struct Unpopulated;
struct Dog;
struct Cat;
@ -167,6 +168,41 @@ where
}
}
impl<S> GraphQLType<S> for Unpopulated
where
S: ScalarValue,
{
fn name(_: &()) -> Option<&'static str> {
Some("Unpopulated")
}
fn meta<'r>(i: &(), registry: &mut Registry<'r, S>) -> MetaType<'r, S>
where
S: 'r,
{
let fields = &[registry
.field::<Option<String>>("name", i)
.argument(registry.arg::<Option<bool>>("surname", i))];
registry
.build_interface_type::<Self>(i, fields)
.interfaces(&[registry.get_type::<Being>(i)])
.into_meta()
}
}
impl<S> GraphQLValue<S> for Unpopulated
where
S: ScalarValue,
{
type Context = ();
type TypeInfo = ();
fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> {
<Self as GraphQLType>::name(info)
}
}
impl<S> GraphQLType<S> for DogCommand
where
S: ScalarValue,
@ -777,6 +813,8 @@ where
where
S: 'r,
{
let _ = registry.get_type::<Unpopulated>(i);
let fields = [registry.field::<i32>("testInput", i).argument(
registry.arg_with_default::<TestInput>(
"input",

View file

@ -55,7 +55,8 @@ fn expand_on_trait(
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| trait_ident.unraw().to_string());
.unwrap_or_else(|| trait_ident.unraw().to_string())
.into_boxed_str();
if !attr.is_internal && name.starts_with("__") {
ERR.no_double_underscore(
attr.name
@ -120,17 +121,22 @@ fn expand_on_trait(
enum_ident,
enum_alias_ident,
name,
description: attr.description.as_deref().cloned(),
description: attr.description.map(|d| d.into_inner().into_boxed_str()),
context,
scalar,
fields,
implemented_for: attr
.implemented_for
.iter()
.map(|c| c.inner().clone())
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
implements: attr
.implements
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
suppress_dead_code: None,
src_intra_doc_link: format!("trait@{}", trait_ident),
src_intra_doc_link: format!("trait@{}", trait_ident).into_boxed_str(),
};
Ok(quote! {
@ -242,7 +248,8 @@ fn expand_on_derive_input(
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| struct_ident.unraw().to_string());
.unwrap_or_else(|| struct_ident.unraw().to_string())
.into_boxed_str();
if !attr.is_internal && name.starts_with("__") {
ERR.no_double_underscore(
attr.name
@ -301,17 +308,22 @@ fn expand_on_derive_input(
enum_ident,
enum_alias_ident,
name,
description: attr.description.as_deref().cloned(),
description: attr.description.map(|d| d.into_inner().into_boxed_str()),
context,
scalar,
fields,
implemented_for: attr
.implemented_for
.iter()
.map(|c| c.inner().clone())
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
implements: attr
.implements
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
suppress_dead_code: None,
src_intra_doc_link: format!("struct@{}", struct_ident),
src_intra_doc_link: format!("struct@{}", struct_ident).into_boxed_str(),
};
Ok(quote! {

View file

@ -33,7 +33,8 @@ pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| struct_ident.unraw().to_string());
.unwrap_or_else(|| struct_ident.unraw().to_string())
.into_boxed_str();
if !attr.is_internal && name.starts_with("__") {
ERR.no_double_underscore(
attr.name
@ -93,17 +94,22 @@ pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
enum_ident,
enum_alias_ident,
name,
description: attr.description.as_deref().cloned(),
description: attr.description.map(|d| d.into_inner().into_boxed_str()),
context,
scalar,
fields,
implemented_for: attr
.implemented_for
.iter()
.map(|c| c.inner().clone())
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
implements: attr
.implements
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
suppress_dead_code: Some((ast.ident.clone(), data.fields.clone())),
src_intra_doc_link: format!("struct@{}", struct_ident),
src_intra_doc_link: format!("struct@{}", struct_ident).into_boxed_str(),
}
.into_token_stream())
}

View file

@ -81,13 +81,20 @@ struct Attr {
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
r#enum: Option<SpanContainer<syn::Ident>>,
/// Explicitly specified Rust types of [GraphQL objects][2] implementing
/// this [GraphQL interface][1] type.
/// Explicitly specified Rust types of [GraphQL objects][2] or
/// [interfaces][1] implementing this [GraphQL interface][1] type.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
/// [2]: https://spec.graphql.org/June2018/#sec-Objects
implemented_for: HashSet<SpanContainer<syn::TypePath>>,
/// Explicitly specified [GraphQL interfaces, implemented][1] by this
/// [GraphQL interface][0].
///
/// [0]: https://spec.graphql.org/October2021#sec-Interfaces
/// [1]: https://spec.graphql.org/October2021#sel-GAHbhBDABAB_E-0b
implements: HashSet<SpanContainer<syn::TypePath>>,
/// Explicitly specified type of [`Context`] to use for resolving this
/// [GraphQL interface][1] type with.
///
@ -185,6 +192,18 @@ impl Parse for Attr {
.none_or_else(|_| err::dup_arg(impler_span))?;
}
}
"impl" | "implements" => {
input.parse::<token::Eq>()?;
for iface in input.parse_maybe_wrapped_and_punctuated::<
syn::TypePath, token::Bracket, token::Comma,
>()? {
let iface_span = iface.span();
out
.implements
.replace(SpanContainer::new(ident.span(), Some(iface_span), iface))
.none_or_else(|_| err::dup_arg(iface_span))?;
}
}
"enum" => {
input.parse::<token::Eq>()?;
let alias = input.parse::<syn::Ident>()?;
@ -232,6 +251,7 @@ impl Attr {
context: try_merge_opt!(context: self, another),
scalar: try_merge_opt!(scalar: self, another),
implemented_for: try_merge_hashset!(implemented_for: self, another => span_joined),
implements: try_merge_hashset!(implements: self, another => span_joined),
r#enum: try_merge_opt!(r#enum: self, another),
asyncness: try_merge_opt!(asyncness: self, another),
rename_fields: try_merge_opt!(rename_fields: self, another),
@ -283,15 +303,15 @@ struct Definition {
/// [`implementers`]: Self::implementers
enum_alias_ident: syn::Ident,
/// Name of this [GraphQL interface][1] in GraphQL schema.
/// Name of this [GraphQL interface][0] in GraphQL schema.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
name: String,
/// [0]: https://spec.graphql.org/October2021#sec-Interfaces
name: Box<str>,
/// Description of this [GraphQL interface][1] to put into GraphQL schema.
/// Description of this [GraphQL interface][0] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
description: Option<String>,
/// [0]: https://spec.graphql.org/October2021#sec-Interfaces
description: Option<Box<str>>,
/// Rust type of [`Context`] to generate [`GraphQLType`] implementation with
/// for this [GraphQL interface][1].
@ -320,6 +340,12 @@ struct Definition {
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
implemented_for: Vec<syn::TypePath>,
/// [GraphQL interfaces implemented][1] by this [GraphQL interface][0].
///
/// [0]: https://spec.graphql.org/October2021#sec-Interfaces
/// [1]: https://spec.graphql.org/October2021#sel-GAHbhBDABAB_E-0b
implements: Vec<syn::TypePath>,
/// Unlike `#[graphql_interface]` maro, `#[derive(GraphQLInterface)]` can't
/// append `#[allow(dead_code)]` to the unused struct, representing
/// [GraphQL interface][1]. We generate hacky `const` which doesn't actually
@ -329,10 +355,10 @@ struct Definition {
suppress_dead_code: Option<(syn::Ident, syn::Fields)>,
/// Intra-doc link to the [`syn::Item`] defining this
/// [GraphQL interface][1].
/// [GraphQL interface][0].
///
/// [1]: https://spec.graphql.org/June2018/#sec-Interfaces
src_intra_doc_link: String,
/// [0]: https://spec.graphql.org/October2021#sec-Interfaces
src_intra_doc_link: Box<str>,
}
impl ToTokens for Definition {
@ -496,11 +522,6 @@ impl Definition {
let (impl_generics, _, where_clause) = gens.split_for_impl();
let (_, ty_generics, _) = self.generics.split_for_impl();
let implemented_for = &self.implemented_for;
let all_impled_for_unique = (implemented_for.len() > 1).then(|| {
quote! { ::juniper::sa::assert_type_ne_all!(#( #implemented_for ),*); }
});
let suppress_dead_code = self.suppress_dead_code.as_ref().map(|(ident, fields)| {
let const_gens = self.const_trait_generics();
let fields = fields.iter().map(|f| &f.ident);
@ -519,6 +540,49 @@ impl Definition {
}}
});
let implemented_for = &self.implemented_for;
let all_impled_for_unique = (implemented_for.len() > 1).then(|| {
quote! { ::juniper::sa::assert_type_ne_all!(#( #implemented_for ),*); }
});
let mark_object_or_interface = self.implemented_for.iter().map(|impl_for| {
quote_spanned! { impl_for.span() =>
trait GraphQLObjectOrInterface<S: ::juniper::ScalarValue, T> {
fn mark();
}
{
struct Object;
impl<S, T> GraphQLObjectOrInterface<S, Object> for T
where
S: ::juniper::ScalarValue,
T: ::juniper::marker::GraphQLObject<S>,
{
fn mark() {
<T as ::juniper::marker::GraphQLObject<S>>::mark()
}
}
}
{
struct Interface;
impl<S, T> GraphQLObjectOrInterface<S, Interface> for T
where
S: ::juniper::ScalarValue,
T: ::juniper::marker::GraphQLInterface<S>,
{
fn mark() {
<T as ::juniper::marker::GraphQLInterface<S>>::mark()
}
}
}
<#impl_for as GraphQLObjectOrInterface<#scalar, _>>::mark();
}
});
quote! {
#[automatically_derived]
impl#impl_generics ::juniper::marker::GraphQLInterface<#scalar>
@ -528,7 +592,7 @@ impl Definition {
fn mark() {
#suppress_dead_code
#all_impled_for_unique
#( <#implemented_for as ::juniper::marker::GraphQLObject<#scalar>>::mark(); )*
#( { #mark_object_or_interface } )*
}
}
}
@ -565,6 +629,25 @@ impl Definition {
generics.replace_type_path_with_defaults(&mut ty);
ty
});
let const_implements = self
.implements
.iter()
.cloned()
.map(|mut ty| {
generics.replace_type_path_with_defaults(&mut ty);
ty
})
.collect::<Vec<_>>();
let transitive_checks = const_impl_for.clone().map(|const_impl_for| {
quote_spanned! { const_impl_for.span() =>
::juniper::assert_transitive_impls!(
#const_scalar,
#ty#ty_const_generics,
#const_impl_for,
#( #const_implements ),*
);
}
});
quote! {
#[automatically_derived]
@ -580,6 +663,12 @@ impl Definition {
#ty#ty_const_generics,
#( #const_impl_for ),*
);
::juniper::assert_implemented_for!(
#const_scalar,
#ty#ty_const_generics,
#( #const_implements ),*
);
#( #transitive_checks )*
}
}
}
@ -612,6 +701,20 @@ impl Definition {
a.cmp(&b)
});
// Sorting is required to preserve/guarantee the order of interfaces registered in schema.
let mut implements = self.implements.clone();
implements.sort_unstable_by(|a, b| {
let (a, b) = (quote!(#a).to_string(), quote!(#b).to_string());
a.cmp(&b)
});
let impl_interfaces = (!implements.is_empty()).then(|| {
quote! {
.interfaces(&[
#( registry.get_type::<#implements>(info), )*
])
}
});
let fields_meta = self.fields.iter().map(|f| f.method_meta_tokens(None));
quote! {
@ -638,6 +741,7 @@ impl Definition {
];
registry.build_interface_type::<#ty#ty_generics>(info, &fields)
#description
#impl_interfaces
.into_meta()
}
}
@ -801,6 +905,7 @@ impl Definition {
fn impl_reflection_traits_tokens(&self) -> TokenStream {
let ty = &self.enum_alias_ident;
let implemented_for = &self.implemented_for;
let implements = &self.implements;
let scalar = &self.scalar;
let name = &self.name;
let fields = self.fields.iter().map(|f| &f.name);
@ -829,6 +934,15 @@ impl Definition {
];
}
#[automatically_derived]
impl#impl_generics ::juniper::macros::reflect::Implements<#scalar>
for #ty#ty_generics
#where_clause
{
const NAMES: ::juniper::macros::reflect::Types =
&[#( <#implements as ::juniper::macros::reflect::BaseType<#scalar>>::NAME ),*];
}
#[automatically_derived]
impl#impl_generics ::juniper::macros::reflect::WrappedType<#scalar>
for #ty#ty_generics

View file

@ -873,6 +873,125 @@ pub fn derive_scalar_value(input: TokenStream) -> TokenStream {
/// }
/// ```
///
/// # Interfaces implementing other interfaces
///
/// GraphQL allows implementing interfaces on other interfaces in addition to
/// objects.
///
/// > __NOTE:__ Every interface has to specify all other interfaces/objects it
/// > implements or is implemented for. Missing one of `for = ` or
/// > `impl = ` attributes is an understandable compile-time error.
///
/// ```rust
/// # extern crate juniper;
/// use juniper::{graphql_interface, graphql_object, ID};
///
/// #[graphql_interface(for = [HumanValue, Luke])]
/// struct Node {
/// id: ID,
/// }
///
/// #[graphql_interface(impl = NodeValue, for = Luke)]
/// struct Human {
/// id: ID,
/// home_planet: String,
/// }
///
/// struct Luke {
/// id: ID,
/// }
///
/// #[graphql_object(impl = [HumanValue, NodeValue])]
/// impl Luke {
/// fn id(&self) -> &ID {
/// &self.id
/// }
///
/// // As `String` and `&str` aren't distinguished by
/// // GraphQL spec, you can use them interchangeably.
/// // Same is applied for `Cow<'a, str>`.
/// // ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
/// fn home_planet() -> &'static str {
/// "Tatooine"
/// }
/// }
/// ```
///
/// # GraphQL subtyping and additional `null`able fields
///
/// GraphQL allows implementers (both objects and other interfaces) to return
/// "subtypes" instead of an original value. Basically, this allows you to
/// impose additional bounds on the implementation.
///
/// Valid "subtypes" are:
/// - interface implementer instead of an interface itself:
/// - `I implements T` in place of a `T`;
/// - `Vec<I implements T>` in place of a `Vec<T>`.
/// - non-`null` value in place of a `null`able:
/// - `T` in place of a `Option<T>`;
/// - `Vec<T>` in place of a `Vec<Option<T>>`.
///
/// These rules are recursively applied, so `Vec<Vec<I implements T>>` is a
/// valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
///
/// Also, GraphQL allows implementers to add `null`able fields, which aren't
/// present on an original interface.
///
/// ```rust
/// # extern crate juniper;
/// use juniper::{graphql_interface, graphql_object, ID};
///
/// #[graphql_interface(for = [HumanValue, Luke])]
/// struct Node {
/// id: ID,
/// }
///
/// #[graphql_interface(for = HumanConnectionValue)]
/// struct Connection {
/// nodes: Vec<NodeValue>,
/// }
///
/// #[graphql_interface(impl = NodeValue, for = Luke)]
/// struct Human {
/// id: ID,
/// home_planet: String,
/// }
///
/// #[graphql_interface(impl = ConnectionValue)]
/// struct HumanConnection {
/// nodes: Vec<HumanValue>,
/// // ^^^^^^^^^^ notice not `NodeValue`
/// // This can happen, because every `Human` is a `Node` too, so we are
/// // just imposing additional bounds, which still can be resolved with
/// // `... on Connection { nodes }`.
/// }
///
/// struct Luke {
/// id: ID,
/// }
///
/// #[graphql_object(impl = [HumanValue, NodeValue])]
/// impl Luke {
/// fn id(&self) -> &ID {
/// &self.id
/// }
///
/// fn home_planet(language: Option<String>) -> &'static str {
/// // ^^^^^^^^^^^^^^
/// // Notice additional `null`able field, which is missing on `Human`.
/// // Resolving `...on Human { homePlanet }` will provide `None` for
/// // this argument.
/// match language.as_deref() {
/// None | Some("en") => "Tatooine",
/// Some("ko") => "타투인",
/// _ => todo!(),
/// }
/// }
/// }
/// #
/// # fn main() {}
/// ```
///
/// # Renaming policy
///
/// By default, all [GraphQL interface][1] fields and their arguments are renamed

View file

@ -45,10 +45,6 @@ impl<T> SpanContainer<T> {
self.val
}
pub fn inner(&self) -> &T {
&self.val
}
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> SpanContainer<U> {
SpanContainer {
expr: self.expr,

View file

@ -60,7 +60,7 @@ error[E0599]: no method named `to_input_value` found for struct `ObjectA` in the
--> fail/input-object/derive_incompatible_object.rs:6:10
|
2 | struct ObjectA {
| -------------- method `to_input_value` not found for this
| ------- method `to_input_value` not found for this struct
...
6 | #[derive(juniper::GraphQLInputObject)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `ObjectA`

View file

@ -0,0 +1,13 @@
use juniper::graphql_interface;
#[graphql_interface(impl = Node2Value, for = Node2Value)]
struct Node1 {
id: String,
}
#[graphql_interface(impl = Node1Value, for = Node1Value)]
struct Node2 {
id: String,
}
fn main() {}

View file

@ -0,0 +1,26 @@
error[E0391]: cycle detected when expanding type alias `Node1Value`
--> fail/interface/struct/attr_cyclic_impl.rs:3:46
|
3 | #[graphql_interface(impl = Node2Value, for = Node2Value)]
| ^^^^^^^^^^
|
note: ...which requires expanding type alias `Node2Value`...
--> fail/interface/struct/attr_cyclic_impl.rs:8:46
|
8 | #[graphql_interface(impl = Node1Value, for = Node1Value)]
| ^^^^^^^^^^
= note: ...which again requires expanding type alias `Node1Value`, completing the cycle
= note: type aliases cannot be recursive
= help: consider using a struct, enum, or union instead to break the cycle
= help: see <https://doc.rust-lang.org/reference/types.html#recursive-types> for more information
note: cycle used when collecting item types in top-level module
--> fail/interface/struct/attr_cyclic_impl.rs:1:1
|
1 | / use juniper::graphql_interface;
2 | |
3 | | #[graphql_interface(impl = Node2Value, for = Node2Value)]
4 | | struct Node1 {
... |
12 | |
13 | | fn main() {}
| |____________^

View file

@ -45,8 +45,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= note: `#[deny(const_err)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
@ -538,8 +538,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #71800 <https://github.com/rust-lang/rust/issues/71800>

View file

@ -0,0 +1,18 @@
use juniper::graphql_interface;
#[graphql_interface(for = Node2Value)]
struct Node1 {
id: String,
}
#[graphql_interface(impl = Node1Value, for = Node3Value)]
struct Node2 {
id: String,
}
#[graphql_interface(impl = Node2Value)]
struct Node3 {
id: String,
}
fn main() {}

View file

@ -0,0 +1,7 @@
error[E0080]: evaluation of constant value failed
--> fail/interface/struct/attr_missing_transitive_impl.rs:8:46
|
8 | #[graphql_interface(impl = Node1Value, for = Node3Value)]
| ^^^^^^^^^^ the evaluated program panicked at 'Failed to implement interface `Node2` on `Node3`: missing `impl = ` for transitive interface `Node1` on `Node3`.', $DIR/fail/interface/struct/attr_missing_transitive_impl.rs:8:46
|
= note: this error originates in the macro `$crate::panic::panic_2015` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,15 @@
use juniper::GraphQLInterface;
#[derive(GraphQLInterface)]
#[graphql(impl = Node2Value, for = Node2Value)]
struct Node1 {
id: String,
}
#[derive(GraphQLInterface)]
#[graphql(impl = Node1Value, for = Node1Value)]
struct Node2 {
id: String,
}
fn main() {}

View file

@ -0,0 +1,26 @@
error[E0391]: cycle detected when expanding type alias `Node1Value`
--> fail/interface/struct/derive_cyclic_impl.rs:4:36
|
4 | #[graphql(impl = Node2Value, for = Node2Value)]
| ^^^^^^^^^^
|
note: ...which requires expanding type alias `Node2Value`...
--> fail/interface/struct/derive_cyclic_impl.rs:10:36
|
10 | #[graphql(impl = Node1Value, for = Node1Value)]
| ^^^^^^^^^^
= note: ...which again requires expanding type alias `Node1Value`, completing the cycle
= note: type aliases cannot be recursive
= help: consider using a struct, enum, or union instead to break the cycle
= help: see <https://doc.rust-lang.org/reference/types.html#recursive-types> for more information
note: cycle used when collecting item types in top-level module
--> fail/interface/struct/derive_cyclic_impl.rs:1:1
|
1 | / use juniper::GraphQLInterface;
2 | |
3 | | #[derive(GraphQLInterface)]
4 | | #[graphql(impl = Node2Value, for = Node2Value)]
... |
14 | |
15 | | fn main() {}
| |____________^

View file

@ -45,8 +45,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= note: `#[deny(const_err)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
@ -538,8 +538,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #71800 <https://github.com/rust-lang/rust/issues/71800>

View file

@ -0,0 +1,21 @@
use juniper::GraphQLInterface;
#[derive(GraphQLInterface)]
#[graphql(for = Node2Value)]
struct Node1 {
id: String,
}
#[derive(GraphQLInterface)]
#[graphql(impl = Node1Value, for = Node3Value)]
struct Node2 {
id: String,
}
#[derive(GraphQLInterface)]
#[graphql(impl = Node2Value)]
struct Node3 {
id: String,
}
fn main() {}

View file

@ -0,0 +1,7 @@
error[E0080]: evaluation of constant value failed
--> fail/interface/struct/derive_missing_transitive_impl.rs:10:36
|
10 | #[graphql(impl = Node1Value, for = Node3Value)]
| ^^^^^^^^^^ the evaluated program panicked at 'Failed to implement interface `Node2` on `Node3`: missing `impl = ` for transitive interface `Node1` on `Node3`.', $DIR/fail/interface/struct/derive_missing_transitive_impl.rs:10:36
|
= note: this error originates in the macro `$crate::panic::panic_2015` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,13 @@
use juniper::graphql_interface;
#[graphql_interface(impl = Node2Value, for = Node2Value)]
trait Node1 {
fn id(&self) -> &str;
}
#[graphql_interface(impl = Node1Value, for = Node1Value)]
trait Node2 {
fn id() -> String;
}
fn main() {}

View file

@ -0,0 +1,26 @@
error[E0391]: cycle detected when expanding type alias `Node1Value`
--> fail/interface/trait/cyclic_impl.rs:3:46
|
3 | #[graphql_interface(impl = Node2Value, for = Node2Value)]
| ^^^^^^^^^^
|
note: ...which requires expanding type alias `Node2Value`...
--> fail/interface/trait/cyclic_impl.rs:8:46
|
8 | #[graphql_interface(impl = Node1Value, for = Node1Value)]
| ^^^^^^^^^^
= note: ...which again requires expanding type alias `Node1Value`, completing the cycle
= note: type aliases cannot be recursive
= help: consider using a struct, enum, or union instead to break the cycle
= help: see <https://doc.rust-lang.org/reference/types.html#recursive-types> for more information
note: cycle used when collecting item types in top-level module
--> fail/interface/trait/cyclic_impl.rs:1:1
|
1 | / use juniper::graphql_interface;
2 | |
3 | | #[graphql_interface(impl = Node2Value, for = Node2Value)]
4 | | trait Node1 {
... |
12 | |
13 | | fn main() {}
| |____________^

View file

@ -45,8 +45,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= note: `#[deny(const_err)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
@ -538,8 +538,8 @@ error: any use of this value will cause an error
| ^^
| |
| referenced constant has errors
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36
| inside `<CharacterValueEnum<ObjA> as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:814:59
|
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #71800 <https://github.com/rust-lang/rust/issues/71800>

View file

@ -0,0 +1,18 @@
use juniper::graphql_interface;
#[graphql_interface(for = Node2Value)]
trait Node1 {
fn id() -> String;
}
#[graphql_interface(impl = Node1Value, for = Node3Value)]
trait Node2 {
fn id(&self) -> &str;
}
#[graphql_interface(impl = Node2Value)]
trait Node3 {
fn id() -> &'static str;
}
fn main() {}

View file

@ -0,0 +1,7 @@
error[E0080]: evaluation of constant value failed
--> fail/interface/trait/missing_transitive_impl.rs:8:46
|
8 | #[graphql_interface(impl = Node1Value, for = Node3Value)]
| ^^^^^^^^^^ the evaluated program panicked at 'Failed to implement interface `Node2` on `Node3`: missing `impl = ` for transitive interface `Node1` on `Node3`.', $DIR/fail/interface/trait/missing_transitive_impl.rs:8:46
|
= note: this error originates in the macro `$crate::panic::panic_2015` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -4,7 +4,7 @@ use std::marker::PhantomData;
use juniper::{
execute, graphql_interface, graphql_object, graphql_value, graphql_vars, DefaultScalarValue,
FieldError, FieldResult, GraphQLObject, GraphQLUnion, IntoFieldError, ScalarValue,
FieldError, FieldResult, GraphQLObject, GraphQLUnion, IntoFieldError, ScalarValue, ID,
};
use crate::util::{schema, schema_with_scalar};
@ -2539,6 +2539,537 @@ mod nullable_argument_subtyping {
}
}
mod simple_subtyping {
use super::*;
#[graphql_interface(for = [ResourceValue, Endpoint])]
struct Node {
id: Option<ID>,
}
#[graphql_interface(impl = NodeValue, for = Endpoint)]
struct Resource {
id: ID,
url: Option<String>,
}
#[derive(GraphQLObject)]
#[graphql(impl = [ResourceValue, NodeValue])]
struct Endpoint {
id: ID,
url: String,
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn node() -> NodeValue {
Endpoint {
id: ID::from("1".to_owned()),
url: "2".to_owned(),
}
.into()
}
fn resource() -> ResourceValue {
Endpoint {
id: ID::from("3".to_owned()),
url: "4".to_owned(),
}
.into()
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_node() {
const DOC: &str = r#"{
node {
id
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"node": {"id": "1"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_node_on_resource() {
const DOC: &str = r#"{
node {
... on Resource {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_node_on_endpoint() {
const DOC: &str = r#"{
node {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource() {
const DOC: &str = r#"{
resource {
id
url
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource_on_endpoint() {
const DOC: &str = r#"{
resource {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn registers_possible_types() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
possibleTypes {{
kind
name
}}
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"possibleTypes": [
{"kind": "OBJECT", "name": "Endpoint"},
]}}),
vec![],
)),
);
}
}
#[tokio::test]
async fn registers_interfaces() {
let schema = schema(QueryRoot);
for (name, interfaces) in [
("Node", graphql_value!([])),
(
"Resource",
graphql_value!([{"kind": "INTERFACE", "name": "Node"}]),
),
(
"Endpoint",
graphql_value!([
{"kind": "INTERFACE", "name": "Node"},
{"kind": "INTERFACE", "name": "Resource"},
]),
),
] {
let doc = format!(
r#"{{
__type(name: "{}") {{
interfaces {{
kind
name
}}
}}
}}"#,
name,
);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"interfaces": interfaces}}),
vec![],
)),
);
}
}
}
mod branching_subtyping {
use super::*;
#[graphql_interface(for = [HumanValue, DroidValue, Luke, R2D2])]
struct Node {
id: ID,
}
#[graphql_interface(for = [HumanConnection, DroidConnection])]
struct Connection {
nodes: Vec<NodeValue>,
}
#[graphql_interface(impl = NodeValue, for = Luke)]
struct Human {
id: ID,
home_planet: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct HumanConnection {
nodes: Vec<HumanValue>,
}
#[graphql_interface(impl = NodeValue, for = R2D2)]
struct Droid {
id: ID,
primary_function: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct DroidConnection {
nodes: Vec<DroidValue>,
}
#[derive(GraphQLObject)]
#[graphql(impl = [HumanValue, NodeValue])]
struct Luke {
id: ID,
home_planet: String,
father: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = [DroidValue, NodeValue])]
struct R2D2 {
id: ID,
primary_function: String,
charge: f64,
}
enum QueryRoot {
Luke,
R2D2,
}
#[graphql_object]
impl QueryRoot {
fn crew(&self) -> ConnectionValue {
match self {
Self::Luke => HumanConnection {
nodes: vec![Luke {
id: ID::new("1"),
home_planet: "earth".to_owned(),
father: "SPOILER".to_owned(),
}
.into()],
}
.into(),
Self::R2D2 => DroidConnection {
nodes: vec![R2D2 {
id: ID::new("2"),
primary_function: "roll".to_owned(),
charge: 146.0,
}
.into()],
}
.into(),
}
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Connection", "Human", "Droid"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_human_connection() {
const DOC: &str = r#"{
crew {
... on HumanConnection {
nodes {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_human() {
const DOC: &str = r#"{
crew {
nodes {
... on Human {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_luke() {
const DOC: &str = r#"{
crew {
nodes {
... on Luke {
id
homePlanet
father
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
"father": "SPOILER",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid_connection() {
const DOC: &str = r#"{
crew {
... on DroidConnection {
nodes {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid() {
const DOC: &str = r#"{
crew {
nodes {
... on Droid {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_r2d2() {
const DOC: &str = r#"{
crew {
nodes {
... on R2D2 {
id
primaryFunction
charge
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
"charge": 146.0,
}],
}}),
vec![],
)),
);
}
}
mod preserves_visibility {
use super::*;

View file

@ -3,7 +3,7 @@
use juniper::{
execute, graphql_interface, graphql_object, graphql_value, graphql_vars, DefaultScalarValue,
Executor, FieldError, FieldResult, GraphQLInputObject, GraphQLObject, GraphQLUnion,
IntoFieldError, ScalarValue,
IntoFieldError, ScalarValue, ID,
};
use crate::util::{schema, schema_with_scalar};
@ -3378,6 +3378,537 @@ mod nullable_argument_subtyping {
}
}
mod simple_subtyping {
use super::*;
#[graphql_interface(for = [ResourceValue, Endpoint])]
trait Node {
fn id() -> Option<ID>;
}
#[graphql_interface(impl = NodeValue, for = Endpoint)]
trait Resource {
fn id(&self) -> &ID;
fn url(&self) -> Option<&str>;
}
#[derive(GraphQLObject)]
#[graphql(impl = [ResourceValue, NodeValue])]
struct Endpoint {
id: ID,
url: String,
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn node() -> NodeValue {
Endpoint {
id: ID::from("1".to_owned()),
url: "2".to_owned(),
}
.into()
}
fn resource() -> ResourceValue {
Endpoint {
id: ID::from("3".to_owned()),
url: "4".to_owned(),
}
.into()
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_node() {
const DOC: &str = r#"{
node {
id
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"node": {"id": "1"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_node_on_resource() {
const DOC: &str = r#"{
node {
... on Resource {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_node_on_endpoint() {
const DOC: &str = r#"{
node {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource() {
const DOC: &str = r#"{
resource {
id
url
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource_on_endpoint() {
const DOC: &str = r#"{
resource {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn registers_possible_types() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
possibleTypes {{
kind
name
}}
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"possibleTypes": [
{"kind": "OBJECT", "name": "Endpoint"},
]}}),
vec![],
)),
);
}
}
#[tokio::test]
async fn registers_interfaces() {
let schema = schema(QueryRoot);
for (name, interfaces) in [
("Node", graphql_value!([])),
(
"Resource",
graphql_value!([{"kind": "INTERFACE", "name": "Node"}]),
),
(
"Endpoint",
graphql_value!([
{"kind": "INTERFACE", "name": "Node"},
{"kind": "INTERFACE", "name": "Resource"},
]),
),
] {
let doc = format!(
r#"{{
__type(name: "{}") {{
interfaces {{
kind
name
}}
}}
}}"#,
name,
);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"interfaces": interfaces}}),
vec![],
)),
);
}
}
}
mod branching_subtyping {
use super::*;
#[graphql_interface(for = [HumanValue, DroidValue, Luke, R2D2])]
trait Node {
fn id() -> ID;
}
#[graphql_interface(for = [HumanConnection, DroidConnection])]
trait Connection {
fn nodes(&self) -> &[NodeValue];
}
#[graphql_interface(impl = NodeValue, for = Luke)]
trait Human {
fn id(&self) -> &ID;
fn home_planet(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct HumanConnection {
nodes: Vec<HumanValue>,
}
#[graphql_interface(impl = NodeValue, for = R2D2)]
trait Droid {
fn id() -> ID;
fn primary_function() -> String;
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct DroidConnection {
nodes: Vec<DroidValue>,
}
#[derive(GraphQLObject)]
#[graphql(impl = [HumanValue, NodeValue])]
struct Luke {
id: ID,
home_planet: String,
father: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = [DroidValue, NodeValue])]
struct R2D2 {
id: ID,
primary_function: String,
charge: f64,
}
enum QueryRoot {
Luke,
R2D2,
}
#[graphql_object]
impl QueryRoot {
fn crew(&self) -> ConnectionValue {
match self {
Self::Luke => HumanConnection {
nodes: vec![Luke {
id: ID::new("1"),
home_planet: "earth".to_owned(),
father: "SPOILER".to_owned(),
}
.into()],
}
.into(),
Self::R2D2 => DroidConnection {
nodes: vec![R2D2 {
id: ID::new("2"),
primary_function: "roll".to_owned(),
charge: 146.0,
}
.into()],
}
.into(),
}
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Connection", "Human", "Droid"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_human_connection() {
const DOC: &str = r#"{
crew {
... on HumanConnection {
nodes {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_human() {
const DOC: &str = r#"{
crew {
nodes {
... on Human {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_luke() {
const DOC: &str = r#"{
crew {
nodes {
... on Luke {
id
homePlanet
father
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
"father": "SPOILER",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid_connection() {
const DOC: &str = r#"{
crew {
... on DroidConnection {
nodes {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid() {
const DOC: &str = r#"{
crew {
nodes {
... on Droid {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_r2d2() {
const DOC: &str = r#"{
crew {
nodes {
... on R2D2 {
id
primaryFunction
charge
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
"charge": 146.0,
}],
}}),
vec![],
)),
);
}
}
mod preserves_visibility {
use super::*;

View file

@ -4,7 +4,7 @@ use std::marker::PhantomData;
use juniper::{
execute, graphql_object, graphql_value, graphql_vars, DefaultScalarValue, FieldError,
FieldResult, GraphQLInterface, GraphQLObject, GraphQLUnion, IntoFieldError, ScalarValue,
FieldResult, GraphQLInterface, GraphQLObject, GraphQLUnion, IntoFieldError, ScalarValue, ID,
};
use crate::util::{schema, schema_with_scalar};
@ -2559,6 +2559,543 @@ mod nullable_argument_subtyping {
}
}
mod simple_subtyping {
use super::*;
#[derive(GraphQLInterface)]
#[graphql(for = [ResourceValue, Endpoint])]
struct Node {
id: Option<ID>,
}
#[derive(GraphQLInterface)]
#[graphql(impl = NodeValue, for = Endpoint)]
struct Resource {
id: ID,
url: Option<String>,
}
#[derive(GraphQLObject)]
#[graphql(impl = [ResourceValue, NodeValue])]
struct Endpoint {
id: ID,
url: String,
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn node() -> NodeValue {
Endpoint {
id: ID::from("1".to_owned()),
url: "2".to_owned(),
}
.into()
}
fn resource() -> ResourceValue {
Endpoint {
id: ID::from("3".to_owned()),
url: "4".to_owned(),
}
.into()
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_node() {
const DOC: &str = r#"{
node {
id
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"node": {"id": "1"}}), vec![])),
);
}
#[tokio::test]
async fn resolves_node_on_resource() {
const DOC: &str = r#"{
node {
... on Resource {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_node_on_endpoint() {
const DOC: &str = r#"{
node {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"node": {
"id": "1",
"url": "2",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource() {
const DOC: &str = r#"{
resource {
id
url
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_resource_on_endpoint() {
const DOC: &str = r#"{
resource {
... on Endpoint {
id
url
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"resource": {
"id": "3",
"url": "4",
}}),
vec![],
)),
);
}
#[tokio::test]
async fn registers_possible_types() {
for name in ["Node", "Resource"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
possibleTypes {{
kind
name
}}
}}
}}"#,
name,
);
let schema = schema(QueryRoot);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"possibleTypes": [
{"kind": "OBJECT", "name": "Endpoint"},
]}}),
vec![],
)),
);
}
}
#[tokio::test]
async fn registers_interfaces() {
let schema = schema(QueryRoot);
for (name, interfaces) in [
("Node", graphql_value!([])),
(
"Resource",
graphql_value!([{"kind": "INTERFACE", "name": "Node"}]),
),
(
"Endpoint",
graphql_value!([
{"kind": "INTERFACE", "name": "Node"},
{"kind": "INTERFACE", "name": "Resource"},
]),
),
] {
let doc = format!(
r#"{{
__type(name: "{}") {{
interfaces {{
kind
name
}}
}}
}}"#,
name,
);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"interfaces": interfaces}}),
vec![],
)),
);
}
}
}
mod branching_subtyping {
use super::*;
#[derive(GraphQLInterface)]
#[graphql(for = [HumanValue, DroidValue, Luke, R2D2])]
struct Node {
id: ID,
}
#[derive(GraphQLInterface)]
#[graphql(for = [HumanConnection, DroidConnection])]
struct Connection {
nodes: Vec<NodeValue>,
}
#[derive(GraphQLInterface)]
#[graphql(impl = NodeValue, for = Luke)]
struct Human {
id: ID,
home_planet: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct HumanConnection {
nodes: Vec<HumanValue>,
}
#[derive(GraphQLInterface)]
#[graphql(impl = NodeValue, for = R2D2)]
struct Droid {
id: ID,
primary_function: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = ConnectionValue)]
struct DroidConnection {
nodes: Vec<DroidValue>,
}
#[derive(GraphQLObject)]
#[graphql(impl = [HumanValue, NodeValue])]
struct Luke {
id: ID,
home_planet: String,
father: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = [DroidValue, NodeValue])]
struct R2D2 {
id: ID,
primary_function: String,
charge: f64,
}
enum QueryRoot {
Luke,
R2D2,
}
#[graphql_object]
impl QueryRoot {
fn crew(&self) -> ConnectionValue {
match self {
Self::Luke => HumanConnection {
nodes: vec![Luke {
id: ID::new("1"),
home_planet: "earth".to_owned(),
father: "SPOILER".to_owned(),
}
.into()],
}
.into(),
Self::R2D2 => DroidConnection {
nodes: vec![R2D2 {
id: ID::new("2"),
primary_function: "roll".to_owned(),
charge: 146.0,
}
.into()],
}
.into(),
}
}
}
#[tokio::test]
async fn is_graphql_interface() {
for name in ["Node", "Connection", "Human", "Droid"] {
let doc = format!(
r#"{{
__type(name: "{}") {{
kind
}}
}}"#,
name,
);
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(&doc, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INTERFACE"}}), vec![])),
);
}
}
#[tokio::test]
async fn resolves_human_connection() {
const DOC: &str = r#"{
crew {
... on HumanConnection {
nodes {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_human() {
const DOC: &str = r#"{
crew {
nodes {
... on Human {
id
homePlanet
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_luke() {
const DOC: &str = r#"{
crew {
nodes {
... on Luke {
id
homePlanet
father
}
}
}
}"#;
let schema = schema(QueryRoot::Luke);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "1",
"homePlanet": "earth",
"father": "SPOILER",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid_connection() {
const DOC: &str = r#"{
crew {
... on DroidConnection {
nodes {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_droid() {
const DOC: &str = r#"{
crew {
nodes {
... on Droid {
id
primaryFunction
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
}],
}}),
vec![],
)),
);
}
#[tokio::test]
async fn resolves_r2d2() {
const DOC: &str = r#"{
crew {
nodes {
... on R2D2 {
id
primaryFunction
charge
}
}
}
}"#;
let schema = schema(QueryRoot::R2D2);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"crew": {
"nodes": [{
"id": "2",
"primaryFunction": "roll",
"charge": 146.0,
}],
}}),
vec![],
)),
);
}
}
mod preserves_visibility {
use super::*;