Skip to content

Commit

Permalink
Allow interfaces to implement other interfaces (#1028, #1000)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilslv authored Jun 27, 2022
1 parent bd04122 commit 9ca2364
Show file tree
Hide file tree
Showing 34 changed files with 2,440 additions and 61 deletions.
184 changes: 184 additions & 0 deletions book/src/types/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion juniper/src/executor_tests/introspection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
32 changes: 32 additions & 0 deletions juniper/src/macros/reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions juniper/src/schema/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -587,6 +589,7 @@ impl<'a, S> InterfaceMeta<'a, S> {
name,
description: None,
fields: fields.to_vec(),
interface_names: Vec::new(),
}
}

Expand All @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions juniper/src/schema/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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))
Expand Down
7 changes: 5 additions & 2 deletions juniper/src/schema/translate/graphql_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions juniper/src/tests/schema_introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,7 @@ pub(crate) fn schema_introspection_result() -> Value {
}
],
"inputFields": null,
"interfaces": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": [
{
Expand Down Expand Up @@ -2500,7 +2500,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
}
],
"inputFields": null,
"interfaces": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": [
{
Expand Down
Loading

0 comments on commit 9ca2364

Please sign in to comment.