parent
bd041222df
commit
9ca2364bfe
34 changed files with 2440 additions and 61 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`
|
||||
|
|
13
tests/codegen/fail/interface/struct/attr_cyclic_impl.rs
Normal file
13
tests/codegen/fail/interface/struct/attr_cyclic_impl.rs
Normal 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() {}
|
26
tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr
Normal file
26
tests/codegen/fail/interface/struct/attr_cyclic_impl.stderr
Normal 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() {}
|
||||
| |____________^
|
|
@ -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>
|
||||
|
|
|
@ -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() {}
|
|
@ -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)
|
15
tests/codegen/fail/interface/struct/derive_cyclic_impl.rs
Normal file
15
tests/codegen/fail/interface/struct/derive_cyclic_impl.rs
Normal 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() {}
|
|
@ -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() {}
|
||||
| |____________^
|
|
@ -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>
|
||||
|
|
|
@ -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() {}
|
|
@ -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)
|
13
tests/codegen/fail/interface/trait/cyclic_impl.rs
Normal file
13
tests/codegen/fail/interface/trait/cyclic_impl.rs
Normal 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() {}
|
26
tests/codegen/fail/interface/trait/cyclic_impl.stderr
Normal file
26
tests/codegen/fail/interface/trait/cyclic_impl.stderr
Normal 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() {}
|
||||
| |____________^
|
|
@ -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>
|
||||
|
|
|
@ -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() {}
|
|
@ -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)
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
Loading…
Reference in a new issue