Skip to content

Commit 9ca2364

Browse files
authored
Allow interfaces to implement other interfaces (#1028, #1000)
1 parent bd04122 commit 9ca2364

34 files changed

+2440
-61
lines changed

book/src/types/interfaces.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,190 @@ struct Human {
7777
```
7878

7979

80+
### Interfaces implementing other interfaces
81+
82+
GraphQL allows implementing interfaces on other interfaces in addition to objects.
83+
84+
```rust
85+
# extern crate juniper;
86+
use juniper::{graphql_interface, graphql_object, ID};
87+
88+
#[graphql_interface(for = [HumanValue, Luke])]
89+
struct Node {
90+
id: ID,
91+
}
92+
93+
#[graphql_interface(impl = NodeValue, for = Luke)]
94+
struct Human {
95+
id: ID,
96+
home_planet: String,
97+
}
98+
99+
struct Luke {
100+
id: ID,
101+
}
102+
103+
#[graphql_object(impl = [HumanValue, NodeValue])]
104+
impl Luke {
105+
fn id(&self) -> &ID {
106+
&self.id
107+
}
108+
109+
// As `String` and `&str` aren't distinguished by
110+
// GraphQL spec, you can use them interchangeably.
111+
// Same is applied for `Cow<'a, str>`.
112+
// ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
113+
fn home_planet() -> &'static str {
114+
"Tatooine"
115+
}
116+
}
117+
#
118+
# fn main() {}
119+
```
120+
121+
> __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.
122+
123+
```compile_fail
124+
# extern crate juniper;
125+
use juniper::{graphql_interface, GraphQLObject};
126+
127+
#[derive(GraphQLObject)]
128+
pub struct ObjA {
129+
id: String,
130+
}
131+
132+
#[graphql_interface(for = ObjA)]
133+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at
134+
// 'Failed to implement interface `Character` on `ObjA`: missing interface reference in implementer's `impl` attribute.'
135+
struct Character {
136+
id: String,
137+
}
138+
139+
fn main() {}
140+
```
141+
142+
143+
### GraphQL subtyping and additional `null`able fields
144+
145+
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.
146+
147+
Valid "subtypes" are:
148+
- interface implementer instead of an interface itself:
149+
- `I implements T` in place of a `T`;
150+
- `Vec<I implements T>` in place of a `Vec<T>`.
151+
- non-null value in place of a nullable:
152+
- `T` in place of a `Option<T>`;
153+
- `Vec<T>` in place of a `Vec<Option<T>>`.
154+
155+
These rules are recursively applied, so `Vec<Vec<I implements T>>` is a valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
156+
157+
Also, GraphQL allows implementers to add `null`able fields, which aren't present on an original interface.
158+
159+
```rust
160+
# extern crate juniper;
161+
use juniper::{graphql_interface, graphql_object, ID};
162+
163+
#[graphql_interface(for = [HumanValue, Luke])]
164+
struct Node {
165+
id: ID,
166+
}
167+
168+
#[graphql_interface(for = HumanConnectionValue)]
169+
struct Connection {
170+
nodes: Vec<NodeValue>,
171+
}
172+
173+
#[graphql_interface(impl = NodeValue, for = Luke)]
174+
struct Human {
175+
id: ID,
176+
home_planet: String,
177+
}
178+
179+
#[graphql_interface(impl = ConnectionValue)]
180+
struct HumanConnection {
181+
nodes: Vec<HumanValue>,
182+
// ^^^^^^^^^^ notice not `NodeValue`
183+
// This can happen, because every `Human` is a `Node` too, so we are just
184+
// imposing additional bounds, which still can be resolved with
185+
// `... on Connection { nodes }`.
186+
}
187+
188+
struct Luke {
189+
id: ID,
190+
}
191+
192+
#[graphql_object(impl = [HumanValue, NodeValue])]
193+
impl Luke {
194+
fn id(&self) -> &ID {
195+
&self.id
196+
}
197+
198+
fn home_planet(language: Option<String>) -> &'static str {
199+
// ^^^^^^^^^^^^^^
200+
// Notice additional `null`able field, which is missing on `Human`.
201+
// Resolving `...on Human { homePlanet }` will provide `None` for this
202+
// argument.
203+
match language.as_deref() {
204+
None | Some("en") => "Tatooine",
205+
Some("ko") => "타투인",
206+
_ => todo!(),
207+
}
208+
}
209+
}
210+
#
211+
# fn main() {}
212+
```
213+
214+
Violating GraphQL "subtyping" or additional nullable field rules is a compile-time error.
215+
216+
```compile_fail
217+
# extern crate juniper;
218+
use juniper::{graphql_interface, graphql_object};
219+
220+
pub struct ObjA {
221+
id: String,
222+
}
223+
224+
#[graphql_object(impl = CharacterValue)]
225+
impl ObjA {
226+
fn id(&self, is_present: bool) -> &str {
227+
// ^^ the evaluated program panicked at
228+
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument `isPresent` of type `Boolean!`
229+
// isn't present on the interface and so has to be nullable.'
230+
is_present.then(|| self.id.as_str()).unwrap_or("missing")
231+
}
232+
}
233+
234+
#[graphql_interface(for = ObjA)]
235+
struct Character {
236+
id: String,
237+
}
238+
#
239+
# fn main() {}
240+
```
241+
242+
```compile_fail
243+
# extern crate juniper;
244+
use juniper::{graphql_interface, GraphQLObject};
245+
246+
#[derive(GraphQLObject)]
247+
#[graphql(impl = CharacterValue)]
248+
pub struct ObjA {
249+
id: Vec<String>,
250+
// ^^ the evaluated program panicked at
251+
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: implementor is expected to return a subtype of
252+
// interface's return object: `[String!]!` is not a subtype of `String!`.'
253+
}
254+
255+
#[graphql_interface(for = ObjA)]
256+
struct Character {
257+
id: String,
258+
}
259+
#
260+
# fn main() {}
261+
```
262+
263+
80264
### Ignoring trait methods
81265

82266
We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them.

juniper/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
3131
- Forbade default implementations of non-ignored trait methods.
3232
- Supported coercion of additional `null`able arguments and return sub-typing on implementer.
3333
- Supported `rename_all = "<policy>"` attribute argument influencing all its fields and their arguments. ([#971])
34+
- Supported interfaces implementing other interfaces. ([#1028])
3435
- Split `#[derive(GraphQLScalarValue)]` macro into:
3536
- `#[derive(GraphQLScalar)]` for implementing GraphQL scalar: ([#1017])
3637
- Supported generic `ScalarValue`.
@@ -94,6 +95,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
9495
[#1017]: /../../pull/1017
9596
[#1025]: /../../pull/1025
9697
[#1026]: /../../pull/1026
98+
[#1028]: /../../pull/1028
9799
[#1051]: /../../issues/1051
98100
[#1054]: /../../pull/1054
99101
[#1057]: /../../pull/1057

juniper/src/executor_tests/introspection/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ async fn interface_introspection() {
247247
);
248248
assert_eq!(
249249
type_info.get_field_value("interfaces"),
250-
Some(&graphql_value!(null)),
250+
Some(&graphql_value!([])),
251251
);
252252
assert_eq!(
253253
type_info.get_field_value("enumValues"),

juniper/src/macros/reflect.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,38 @@ macro_rules! assert_interfaces_impls {
551551
};
552552
}
553553

554+
/// Asserts that all [transitive interfaces][0] (the ones implemented by the
555+
/// `$interface`) are also implemented by the `$implementor`.
556+
///
557+
/// [0]: https://spec.graphql.org/October2021#sel-FAHbhBHCAACGB35P
558+
#[macro_export]
559+
macro_rules! assert_transitive_impls {
560+
($scalar: ty, $interface: ty, $implementor: ty $(, $transitive: ty)* $(,)?) => {
561+
const _: () = {
562+
$({
563+
let is_present = $crate::macros::reflect::str_exists_in_arr(
564+
<$implementor as ::juniper::macros::reflect::BaseType<$scalar>>::NAME,
565+
<$transitive as ::juniper::macros::reflect::BaseSubTypes<$scalar>>::NAMES,
566+
);
567+
if !is_present {
568+
const MSG: &str = $crate::const_concat!(
569+
"Failed to implement interface `",
570+
<$interface as $crate::macros::reflect::BaseType<$scalar>>::NAME,
571+
"` on `",
572+
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
573+
"`: missing `impl = ` for transitive interface `",
574+
<$transitive as $crate::macros::reflect::BaseType<$scalar>>::NAME,
575+
"` on `",
576+
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
577+
"`."
578+
);
579+
::std::panic!("{}", MSG);
580+
}
581+
})*
582+
};
583+
};
584+
}
585+
554586
/// Asserts validness of [`Field`] [`Arguments`] and returned [`Type`].
555587
///
556588
/// This assertion is a combination of [`assert_subtype`] and

juniper/src/schema/meta.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ pub struct InterfaceMeta<'a, S> {
110110
pub description: Option<String>,
111111
#[doc(hidden)]
112112
pub fields: Vec<Field<'a, S>>,
113+
#[doc(hidden)]
114+
pub interface_names: Vec<String>,
113115
}
114116

115117
/// Union type metadata
@@ -587,6 +589,7 @@ impl<'a, S> InterfaceMeta<'a, S> {
587589
name,
588590
description: None,
589591
fields: fields.to_vec(),
592+
interface_names: Vec::new(),
590593
}
591594
}
592595

@@ -599,6 +602,18 @@ impl<'a, S> InterfaceMeta<'a, S> {
599602
self
600603
}
601604

605+
/// Sets the `interfaces` this [`InterfaceMeta`] interface implements.
606+
///
607+
/// Overwrites any previously set list of interfaces.
608+
#[must_use]
609+
pub fn interfaces(mut self, interfaces: &[Type<'a>]) -> Self {
610+
self.interface_names = interfaces
611+
.iter()
612+
.map(|t| t.innermost_name().to_owned())
613+
.collect();
614+
self
615+
}
616+
602617
/// Wraps this [`InterfaceMeta`] type into a generic [`MetaType`].
603618
pub fn into_meta(self) -> MetaType<'a, S> {
604619
MetaType::Interface(self)

juniper/src/schema/schema.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,16 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
244244

245245
fn interfaces<'s>(&self, context: &'s SchemaType<'a, S>) -> Option<Vec<TypeType<'s, S>>> {
246246
match self {
247-
TypeType::Concrete(&MetaType::Object(ObjectMeta {
248-
ref interface_names,
249-
..
250-
})) => Some(
247+
TypeType::Concrete(
248+
&MetaType::Object(ObjectMeta {
249+
ref interface_names,
250+
..
251+
})
252+
| &MetaType::Interface(InterfaceMeta {
253+
ref interface_names,
254+
..
255+
}),
256+
) => Some(
251257
interface_names
252258
.iter()
253259
.filter_map(|n| context.type_by_name(n))

juniper/src/schema/translate/graphql_parser.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,11 @@ impl GraphQLParserTranslator {
190190
position: Pos::default(),
191191
description: x.description.as_ref().map(|s| From::from(s.as_str())),
192192
name: From::from(x.name.as_ref()),
193-
// TODO: Support this with GraphQL October 2021 Edition.
194-
implements_interfaces: vec![],
193+
implements_interfaces: x
194+
.interface_names
195+
.iter()
196+
.map(|s| From::from(s.as_str()))
197+
.collect(),
195198
directives: vec![],
196199
fields: x
197200
.fields

juniper/src/tests/schema_introspection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ pub(crate) fn schema_introspection_result() -> Value {
11731173
}
11741174
],
11751175
"inputFields": null,
1176-
"interfaces": null,
1176+
"interfaces": [],
11771177
"enumValues": null,
11781178
"possibleTypes": [
11791179
{
@@ -2500,7 +2500,7 @@ pub(crate) fn schema_introspection_result_without_descriptions() -> Value {
25002500
}
25012501
],
25022502
"inputFields": null,
2503-
"interfaces": null,
2503+
"interfaces": [],
25042504
"enumValues": null,
25052505
"possibleTypes": [
25062506
{

0 commit comments

Comments
 (0)