From 9ca2364bfeea228ad5cb94acc90a8853dc99f4fe Mon Sep 17 00:00:00 2001 From: ilslv <47687266+ilslv@users.noreply.github.com> Date: Mon, 27 Jun 2022 15:09:44 +0300 Subject: [PATCH] Allow interfaces to implement other interfaces (#1028, #1000) --- book/src/types/interfaces.md | 184 ++++++ juniper/CHANGELOG.md | 2 + .../src/executor_tests/introspection/mod.rs | 2 +- juniper/src/macros/reflect.rs | 32 ++ juniper/src/schema/meta.rs | 15 + juniper/src/schema/schema.rs | 14 +- .../src/schema/translate/graphql_parser.rs | 7 +- juniper/src/tests/schema_introspection.rs | 4 +- .../rules/possible_fragment_spreads.rs | 56 ++ juniper/src/validation/test_harness.rs | 38 ++ juniper_codegen/src/graphql_interface/attr.rs | 32 +- .../src/graphql_interface/derive.rs | 16 +- juniper_codegen/src/graphql_interface/mod.rs | 148 ++++- juniper_codegen/src/lib.rs | 119 ++++ juniper_codegen/src/util/span_container.rs | 4 - .../derive_incompatible_object.stderr | 2 +- .../fail/interface/struct/attr_cyclic_impl.rs | 13 + .../interface/struct/attr_cyclic_impl.stderr | 26 + .../struct/attr_missing_field.stderr | 8 +- .../struct/attr_missing_transitive_impl.rs | 18 + .../attr_missing_transitive_impl.stderr | 7 + .../interface/struct/derive_cyclic_impl.rs | 15 + .../struct/derive_cyclic_impl.stderr | 26 + .../struct/derive_missing_field.stderr | 8 +- .../struct/derive_missing_transitive_impl.rs | 21 + .../derive_missing_transitive_impl.stderr | 7 + .../fail/interface/trait/cyclic_impl.rs | 13 + .../fail/interface/trait/cyclic_impl.stderr | 26 + .../fail/interface/trait/missing_field.stderr | 8 +- .../trait/missing_transitive_impl.rs | 18 + .../trait/missing_transitive_impl.stderr | 7 + .../src/codegen/interface_attr_struct.rs | 533 ++++++++++++++++- .../src/codegen/interface_attr_trait.rs | 533 ++++++++++++++++- .../src/codegen/interface_derive.rs | 539 +++++++++++++++++- 34 files changed, 2440 insertions(+), 61 deletions(-) create mode 100644 tests/codegen/fail/interface/struct/attr_cyclic_impl.rs create mode 100644 tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr create mode 100644 tests/codegen/fail/interface/struct/attr_missing_transitive_impl.rs create mode 100644 tests/codegen/fail/interface/struct/attr_missing_transitive_impl.stderr create mode 100644 tests/codegen/fail/interface/struct/derive_cyclic_impl.rs create mode 100644 tests/codegen/fail/interface/struct/derive_cyclic_impl.stderr create mode 100644 tests/codegen/fail/interface/struct/derive_missing_transitive_impl.rs create mode 100644 tests/codegen/fail/interface/struct/derive_missing_transitive_impl.stderr create mode 100644 tests/codegen/fail/interface/trait/cyclic_impl.rs create mode 100644 tests/codegen/fail/interface/trait/cyclic_impl.stderr create mode 100644 tests/codegen/fail/interface/trait/missing_transitive_impl.rs create mode 100644 tests/codegen/fail/interface/trait/missing_transitive_impl.stderr diff --git a/book/src/types/interfaces.md b/book/src/types/interfaces.md index aa9c543e7..949344f9f 100644 --- a/book/src/types/interfaces.md +++ b/book/src/types/interfaces.md @@ -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` in place of a `Vec`. +- non-null value in place of a nullable: + - `T` in place of a `Option`; + - `Vec` in place of a `Vec>`. + +These rules are recursively applied, so `Vec>` is a valid "subtype" of a `Option>>>>`. + +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, +} + +#[graphql_interface(impl = NodeValue, for = Luke)] +struct Human { + id: ID, + home_planet: String, +} + +#[graphql_interface(impl = ConnectionValue)] +struct HumanConnection { + nodes: Vec, + // ^^^^^^^^^^ 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) -> &'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, +// ^^ 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. diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 1bff5bcaf..7a5aa27aa 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -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 = ""` 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 diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs index 25eff8452..ed59efc79 100644 --- a/juniper/src/executor_tests/introspection/mod.rs +++ b/juniper/src/executor_tests/introspection/mod.rs @@ -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"), diff --git a/juniper/src/macros/reflect.rs b/juniper/src/macros/reflect.rs index 4cba097f8..a85296b1f 100644 --- a/juniper/src/macros/reflect.rs +++ b/juniper/src/macros/reflect.rs @@ -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 diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 2ba1faa49..eecef5905 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -110,6 +110,8 @@ pub struct InterfaceMeta<'a, S> { pub description: Option, #[doc(hidden)] pub fields: Vec>, + #[doc(hidden)] + pub interface_names: Vec, } /// 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) diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 30906478f..825bb43aa 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -244,10 +244,16 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> { fn interfaces<'s>(&self, context: &'s SchemaType<'a, S>) -> Option>> { match self { - TypeType::Concrete(&MetaType::Object(ObjectMeta { - ref interface_names, - .. - })) => Some( + TypeType::Concrete( + &MetaType::Object(ObjectMeta { + ref interface_names, + .. + }) + | &MetaType::Interface(InterfaceMeta { + ref interface_names, + .. + }), + ) => Some( interface_names .iter() .filter_map(|n| context.type_by_name(n)) diff --git a/juniper/src/schema/translate/graphql_parser.rs b/juniper/src/schema/translate/graphql_parser.rs index dc0ab05da..c3864561c 100644 --- a/juniper/src/schema/translate/graphql_parser.rs +++ b/juniper/src/schema/translate/graphql_parser.rs @@ -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 diff --git a/juniper/src/tests/schema_introspection.rs b/juniper/src/tests/schema_introspection.rs index 1e1b03e0a..d2e4643e5 100644 --- a/juniper/src/tests/schema_introspection.rs +++ b/juniper/src/tests/schema_introspection.rs @@ -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": [ { diff --git a/juniper/src/validation/rules/possible_fragment_spreads.rs b/juniper/src/validation/rules/possible_fragment_spreads.rs index ceb308c91..53ef05a34 100644 --- a/juniper/src/validation/rules/possible_fragment_spreads.rs +++ b/juniper/src/validation/rules/possible_fragment_spreads.rs @@ -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>( diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index 5bba0a73b..1772b639e 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -20,6 +20,7 @@ use crate::{ struct Being; struct Pet; struct Canine; +struct Unpopulated; struct Dog; struct Cat; @@ -167,6 +168,41 @@ where } } +impl GraphQLType 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::>("name", i) + .argument(registry.arg::>("surname", i))]; + + registry + .build_interface_type::(i, fields) + .interfaces(&[registry.get_type::(i)]) + .into_meta() + } +} + +impl GraphQLValue for Unpopulated +where + S: ScalarValue, +{ + type Context = (); + type TypeInfo = (); + + fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { + ::name(info) + } +} + impl GraphQLType for DogCommand where S: ScalarValue, @@ -777,6 +813,8 @@ where where S: 'r, { + let _ = registry.get_type::(i); + let fields = [registry.field::("testInput", i).argument( registry.arg_with_default::( "input", diff --git a/juniper_codegen/src/graphql_interface/attr.rs b/juniper_codegen/src/graphql_interface/attr.rs index 746b7af76..e53c99dda 100644 --- a/juniper_codegen/src/graphql_interface/attr.rs +++ b/juniper_codegen/src/graphql_interface/attr.rs @@ -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! { diff --git a/juniper_codegen/src/graphql_interface/derive.rs b/juniper_codegen/src/graphql_interface/derive.rs index e113bdced..949a18427 100644 --- a/juniper_codegen/src/graphql_interface/derive.rs +++ b/juniper_codegen/src/graphql_interface/derive.rs @@ -33,7 +33,8 @@ pub fn expand(input: TokenStream) -> syn::Result { .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 { 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()) } diff --git a/juniper_codegen/src/graphql_interface/mod.rs b/juniper_codegen/src/graphql_interface/mod.rs index 2114e2cf5..700fa3dc3 100644 --- a/juniper_codegen/src/graphql_interface/mod.rs +++ b/juniper_codegen/src/graphql_interface/mod.rs @@ -81,13 +81,20 @@ struct Attr { /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces r#enum: Option>, - /// 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>, + /// 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>, + /// 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::()?; + 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::()?; let alias = input.parse::()?; @@ -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, - /// 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, + /// [0]: https://spec.graphql.org/October2021#sec-Interfaces + description: Option>, /// 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, + /// [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, + /// 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, } 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 { + fn mark(); + } + + { + struct Object; + + impl GraphQLObjectOrInterface for T + where + S: ::juniper::ScalarValue, + T: ::juniper::marker::GraphQLObject, + { + fn mark() { + >::mark() + } + } + } + + { + struct Interface; + + impl GraphQLObjectOrInterface for T + where + S: ::juniper::ScalarValue, + T: ::juniper::marker::GraphQLInterface, + { + fn mark() { + >::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::>(); + 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 diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 6ab96c2c6..70c346eed 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -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` in place of a `Vec`. +/// - non-`null` value in place of a `null`able: +/// - `T` in place of a `Option`; +/// - `Vec` in place of a `Vec>`. +/// +/// These rules are recursively applied, so `Vec>` is a +/// valid "subtype" of a `Option>>>>`. +/// +/// 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, +/// } +/// +/// #[graphql_interface(impl = NodeValue, for = Luke)] +/// struct Human { +/// id: ID, +/// home_planet: String, +/// } +/// +/// #[graphql_interface(impl = ConnectionValue)] +/// struct HumanConnection { +/// nodes: Vec, +/// // ^^^^^^^^^^ 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) -> &'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 diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs index 370f17a74..8b4c6b58f 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/util/span_container.rs @@ -45,10 +45,6 @@ impl SpanContainer { self.val } - pub fn inner(&self) -> &T { - &self.val - } - pub fn map U>(self, f: F) -> SpanContainer { SpanContainer { expr: self.expr, diff --git a/tests/codegen/fail/input-object/derive_incompatible_object.stderr b/tests/codegen/fail/input-object/derive_incompatible_object.stderr index e8e2a3ccf..6767eead2 100644 --- a/tests/codegen/fail/input-object/derive_incompatible_object.stderr +++ b/tests/codegen/fail/input-object/derive_incompatible_object.stderr @@ -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` diff --git a/tests/codegen/fail/interface/struct/attr_cyclic_impl.rs b/tests/codegen/fail/interface/struct/attr_cyclic_impl.rs new file mode 100644 index 000000000..7681972ae --- /dev/null +++ b/tests/codegen/fail/interface/struct/attr_cyclic_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr b/tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr new file mode 100644 index 000000000..ed809a5a0 --- /dev/null +++ b/tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr @@ -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 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() {} + | |____________^ diff --git a/tests/codegen/fail/interface/struct/attr_missing_field.stderr b/tests/codegen/fail/interface/struct/attr_missing_field.stderr index 429efcea5..37afdb394 100644 --- a/tests/codegen/fail/interface/struct/attr_missing_field.stderr +++ b/tests/codegen/fail/interface/struct/attr_missing_field.stderr @@ -45,8 +45,8 @@ error: any use of this value will cause an error | ^^ | | | referenced constant has errors - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 diff --git a/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.rs b/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.rs new file mode 100644 index 000000000..590d1517e --- /dev/null +++ b/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.stderr b/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.stderr new file mode 100644 index 000000000..389334dc0 --- /dev/null +++ b/tests/codegen/fail/interface/struct/attr_missing_transitive_impl.stderr @@ -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) diff --git a/tests/codegen/fail/interface/struct/derive_cyclic_impl.rs b/tests/codegen/fail/interface/struct/derive_cyclic_impl.rs new file mode 100644 index 000000000..56d77cc62 --- /dev/null +++ b/tests/codegen/fail/interface/struct/derive_cyclic_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/struct/derive_cyclic_impl.stderr b/tests/codegen/fail/interface/struct/derive_cyclic_impl.stderr new file mode 100644 index 000000000..28f960ebe --- /dev/null +++ b/tests/codegen/fail/interface/struct/derive_cyclic_impl.stderr @@ -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 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() {} + | |____________^ diff --git a/tests/codegen/fail/interface/struct/derive_missing_field.stderr b/tests/codegen/fail/interface/struct/derive_missing_field.stderr index b4e096f4a..a9a91ec79 100644 --- a/tests/codegen/fail/interface/struct/derive_missing_field.stderr +++ b/tests/codegen/fail/interface/struct/derive_missing_field.stderr @@ -45,8 +45,8 @@ error: any use of this value will cause an error | ^^ | | | referenced constant has errors - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 diff --git a/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.rs b/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.rs new file mode 100644 index 000000000..446aae214 --- /dev/null +++ b/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.stderr b/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.stderr new file mode 100644 index 000000000..278b725f2 --- /dev/null +++ b/tests/codegen/fail/interface/struct/derive_missing_transitive_impl.stderr @@ -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) diff --git a/tests/codegen/fail/interface/trait/cyclic_impl.rs b/tests/codegen/fail/interface/trait/cyclic_impl.rs new file mode 100644 index 000000000..6f5b79471 --- /dev/null +++ b/tests/codegen/fail/interface/trait/cyclic_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/trait/cyclic_impl.stderr b/tests/codegen/fail/interface/trait/cyclic_impl.stderr new file mode 100644 index 000000000..9865861fd --- /dev/null +++ b/tests/codegen/fail/interface/trait/cyclic_impl.stderr @@ -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 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() {} + | |____________^ diff --git a/tests/codegen/fail/interface/trait/missing_field.stderr b/tests/codegen/fail/interface/trait/missing_field.stderr index 311c80110..45913e165 100644 --- a/tests/codegen/fail/interface/trait/missing_field.stderr +++ b/tests/codegen/fail/interface/trait/missing_field.stderr @@ -45,8 +45,8 @@ error: any use of this value will cause an error | ^^ | | | referenced constant has errors - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as juniper::macros::reflect::Field<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:719:36 - | inside ` as AsyncField<__S, id>>::call::_::RES` at $WORKSPACE/juniper/src/macros/reflect.rs:782:59 + | inside ` as AsyncField<__S, id>>::call::_::check` at $WORKSPACE/juniper/src/macros/reflect.rs:751:36 + | inside ` 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 diff --git a/tests/codegen/fail/interface/trait/missing_transitive_impl.rs b/tests/codegen/fail/interface/trait/missing_transitive_impl.rs new file mode 100644 index 000000000..2bc08fb0a --- /dev/null +++ b/tests/codegen/fail/interface/trait/missing_transitive_impl.rs @@ -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() {} diff --git a/tests/codegen/fail/interface/trait/missing_transitive_impl.stderr b/tests/codegen/fail/interface/trait/missing_transitive_impl.stderr new file mode 100644 index 000000000..f120ac360 --- /dev/null +++ b/tests/codegen/fail/interface/trait/missing_transitive_impl.stderr @@ -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) diff --git a/tests/integration/src/codegen/interface_attr_struct.rs b/tests/integration/src/codegen/interface_attr_struct.rs index 388bae783..8d0e4f58e 100644 --- a/tests/integration/src/codegen/interface_attr_struct.rs +++ b/tests/integration/src/codegen/interface_attr_struct.rs @@ -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, + } + + #[graphql_interface(impl = NodeValue, for = Endpoint)] + struct Resource { + id: ID, + url: Option, + } + + #[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, + } + + #[graphql_interface(impl = NodeValue, for = Luke)] + struct Human { + id: ID, + home_planet: String, + } + + #[derive(GraphQLObject)] + #[graphql(impl = ConnectionValue)] + struct HumanConnection { + nodes: Vec, + } + + #[graphql_interface(impl = NodeValue, for = R2D2)] + struct Droid { + id: ID, + primary_function: String, + } + + #[derive(GraphQLObject)] + #[graphql(impl = ConnectionValue)] + struct DroidConnection { + nodes: Vec, + } + + #[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::*; diff --git a/tests/integration/src/codegen/interface_attr_trait.rs b/tests/integration/src/codegen/interface_attr_trait.rs index a81a758bb..150820fab 100644 --- a/tests/integration/src/codegen/interface_attr_trait.rs +++ b/tests/integration/src/codegen/interface_attr_trait.rs @@ -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; + } + + #[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, + } + + #[graphql_interface(impl = NodeValue, for = R2D2)] + trait Droid { + fn id() -> ID; + fn primary_function() -> String; + } + + #[derive(GraphQLObject)] + #[graphql(impl = ConnectionValue)] + struct DroidConnection { + nodes: Vec, + } + + #[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::*; diff --git a/tests/integration/src/codegen/interface_derive.rs b/tests/integration/src/codegen/interface_derive.rs index 7882f717b..bbc6a79ad 100644 --- a/tests/integration/src/codegen/interface_derive.rs +++ b/tests/integration/src/codegen/interface_derive.rs @@ -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, + } + + #[derive(GraphQLInterface)] + #[graphql(impl = NodeValue, for = Endpoint)] + struct Resource { + id: ID, + url: Option, + } + + #[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, + } + + #[derive(GraphQLInterface)] + #[graphql(impl = NodeValue, for = Luke)] + struct Human { + id: ID, + home_planet: String, + } + + #[derive(GraphQLObject)] + #[graphql(impl = ConnectionValue)] + struct HumanConnection { + nodes: Vec, + } + + #[derive(GraphQLInterface)] + #[graphql(impl = NodeValue, for = R2D2)] + struct Droid { + id: ID, + primary_function: String, + } + + #[derive(GraphQLObject)] + #[graphql(impl = ConnectionValue)] + struct DroidConnection { + nodes: Vec, + } + + #[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::*;