Interfaces
GraphQL interfaces represent a list of named fields and their arguments. GraphQL objects and interfaces can then implement these interfaces which requires that the implementing type will define all fields defined by those interfaces.
GraphQL interfaces map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of GraphQL interfaces are Rust traits, but the main difference is that in GraphQL an interface type serves both as an abstraction and a boxed value (dispatchable to concrete implementers), while in Rust, a trait is an abstraction only, and to represent such a boxed value a separate type is required, like a trait object or an enum consisting of implementer types, because Rust trait doesn't represent a type itself, and so, can have no values.
Another notable difference is that GraphQL interfaces are more like structurally-typed contracts: they only declare a list of fields a GraphQL type should already have. Rust traits, on the other hand, are type classes, which don't really care about existing methods, but, rather, require to provide implementations for required methods despite the fact whether the type already has such methods or not. This difference makes the trait implementation not a good fit for expressing a GraphQL interface implementation, because we don't really need to implement any fields, the GraphQL type implementing a GraphQL interface has those fields already. We only need to check that fields' signatures match.
That's why Juniper takes the following approach to represent GraphQL interfaces, which consists of two parts:
- Either a struct, or a trait (in case fields have arguments), which acts only as a blueprint describing the required list of fields, and is not used in runtime at all.
- An auto-generated enum, representing a dispatchable value-type for the GraphQL interfaces, which may be referred and returned by other fields.
This may be done by using either the #[graphql_interface]
attribute or the #[derive(GraphQLInterface)]
:
extern crate juniper; use juniper::{graphql_interface, GraphQLInterface, GraphQLObject}; // By default a `CharacterValue` enum is generated by macro to represent // values of this GraphQL interface. #[derive(GraphQLInterface)] #[graphql(for = Human)] // enumerating all implementers is mandatory struct Character { id: String, } // Using a trait to describe the required fields is fine too. #[graphql_interface] #[graphql(enum = HasHomeEnum, for = Human)] // ^^^^ the generated value-type enum can be renamed, if required trait HasHome { fn home_planet(&self) -> &str; } #[derive(GraphQLObject)] #[graphql(impl = [CharacterValue, HasHomeEnum])] // ^^^^^^^^^^^^^^ ^^^^^^^^^^^ // Notice the enum type names, neither the trait name nor the struct name // is used to refer the GraphQL interface. struct Human { id: String, // also resolves `Character.id` field home_planet: String, // also resolves `HasHome.homePlanet` field } fn main() {}
Interfaces implementing other interfaces
GraphQL allows implementing interfaces on other interfaces in addition to objects:
extern crate juniper; use juniper::{graphql_object, GraphQLInterface, ID}; #[derive(GraphQLInterface)] #[graphql(for = [HumanValue, Luke])] struct Node { id: ID, } #[derive(GraphQLInterface)] #[graphql(impl = NodeValue, for = Luke)] struct Human { id: ID, home_planet: String, } struct Luke { id: ID, } #[graphql_object] #[graphql(impl = [HumanValue, NodeValue])] impl Luke { fn id(&self) -> &ID { &self.id } // As `String` and `&str` aren't distinguished by GraphQL spec and // represent the same `!String` GraphQL scalar, we can use them // interchangeably. The 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 =
orimpl =
attribute arguments is a compile-time error.extern crate juniper; use juniper::{GraphQLInterface, GraphQLObject}; #[derive(GraphQLObject)] pub struct ObjA { id: String, } #[derive(GraphQLInterface)] #[graphql(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() {}
Subtyping and additional null
able arguments
GraphQL allows implementers (both objects and other interfaces) to return "subtypes" instead of an original value. Basically, this allows to impose additional bounds on the implementation.
Valid "subtypes" are:
- interface implementer instead of an interface itself:
I implements T
in place of aT
;Vec<I implements T>
in place of aVec<T>
.
- non-
null
value in place of anull
able:T
in place of aOption<T>
;Vec<T>
in place of aVec<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 field arguments, which aren't present on an original interface.
extern crate juniper; use juniper::{graphql_interface, graphql_object, GraphQLInterface, ID}; #[derive(GraphQLInterface)] #[graphql(for = [HumanValue, Luke])] struct Node { id: ID, } #[derive(GraphQLInterface)] #[graphql(for = HumanConnectionValue)] struct Connection { nodes: Vec<NodeValue>, } #[derive(GraphQLInterface)] #[graphql(impl = NodeValue, for = Luke)] struct Human { id: ID, home_planet: String, } #[derive(GraphQLInterface)] #[graphql(impl = ConnectionValue)] struct HumanConnection { nodes: Vec<HumanValue>, // ^^^^^^^^^^ notice not `NodeValue` // This can happen, because every `Human` is a `Node` too, so we just // impose additional bounds, which still can be resolved with // `... on Connection { nodes }` syntax. } struct Luke { id: ID, } #[graphql_object] #[graphql(impl = [HumanValue, NodeValue])] impl Luke { fn id(&self) -> &ID { &self.id } fn home_planet(language: Option<String>) -> &'static str { // ^^^^^^^^^^^^^^ // Notice additional `null`able field argument, which is missing on // `Human`. Resolving `...on Human { homePlanet }` will provide `None` // for this argument (default argument value). match language.as_deref() { None | Some("en") => "Tatooine", Some("ko") => "타투인", _ => unimplemented!(), } } } fn main() {}
NOTE: Violating GraphQL "subtyping" or additional
null
able argument rules is a compile-time error.extern crate juniper; use juniper::{graphql_object, GraphQLInterface}; pub struct ObjA { id: String, } #[graphql_object] #[graphql(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_some(&self.id).unwrap_or("missing") } } #[derive(GraphQLInterface)] #[graphql(for = ObjA)] struct Character { id: String, } fn main() {}
extern crate juniper; use juniper::{GraphQLInterface, 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`: // implementer is expected to return a subtype of interface's return // object: `[String!]!` is not a subtype of `String!`.' } #[derive(GraphQLInterface)] #[graphql(for = ObjA)] struct Character { id: String, } fn main() {}
Default arguments
Similarly to GraphQL object fields, GraphQL arguments of interfaces are able to have default values, though Rust doesn't have such notion:
extern crate juniper; use juniper::graphql_interface; #[graphql_interface] trait Person { fn field1( // Default value can be any valid Rust expression, including a function // call, etc. #[graphql(default = true)] arg1: bool, // If default expression is not specified, then the `Default::default()` // value is used. #[graphql(default)] arg2: i32, ) -> String; } fn main() {}
Renaming
Just as with defining GraphQL objects, by default, fields are converted from Rust's standard snake_case
naming convention into GraphQL's camelCase
convention:
We can override the name by using the #[graphql(name = "...")]
attribute:
extern crate juniper; use juniper::{graphql_interface, GraphQLInterface}; #[derive(GraphQLInterface)] #[graphql(name = "CharacterInterface")] struct Character { // exposed as `CharacterInterface` in GraphQL schema #[graphql(name = "myCustomFieldName")] renamed_field: bool, // exposed as `myCustomFieldName` in GraphQL schema } #[graphql_interface] #[graphql(name = "PersonInterface")] trait Person { // exposed as `PersonInterface` in GraphQL schema #[graphql(name = "myCustomFieldName")] fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema #[graphql(name = "myArgument")] renamed_argument: bool, // exposed as `myArgument` in GraphQL schema ) -> bool; } fn main() {}
Or provide a different renaming policy for all the defined fields:
extern crate juniper; use juniper::graphql_interface; #[graphql_interface] #[graphql(rename_all = "none")] // disables any renaming trait Person { fn renamed_field( // exposed as `renamed_field` in GraphQL schema renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema ) -> bool; } fn main() {}
TIP: Supported policies are:
SCREAMING_SNAKE_CASE
,camelCase
andnone
(disables any renaming).
Documentation and deprecation
Similarly, GraphQL fields of interfaces may also be documented and deprecated via #[graphql(description = "...")]
and #[graphql(deprecated = "...")]
/#[deprecated]
attributes:
extern crate juniper; use juniper::graphql_interface; /// This doc comment is visible only in Rust API docs. #[graphql_interface] #[graphql(description = "This description overwrites the one from doc comment.")] trait Person { /// This doc comment is visible only in Rust API docs. #[graphql(description = "This description is visible only in GraphQL schema.")] fn empty() -> &'static str; #[graphql(desc = "This description is visible only in GraphQL schema.")] // ^^^^ shortcut for a `description` argument fn field( #[graphql(desc = "This description is visible only in GraphQL schema.")] arg: bool, ) -> bool; /// This doc comment is visible in both Rust API docs and GraphQL schema /// descriptions. #[graphql(deprecated = "Just because.")] fn deprecated_graphql() -> bool; // Standard Rust's `#[deprecated]` attribute works too! #[deprecated(note = "Reason is optional, btw!")] fn deprecated_standard() -> bool; // has no description in GraphQL schema } fn main() {}
NOTE: Only GraphQL interface/object fields and GraphQL enum values can be deprecated.
Ignoring
By default, all struct fields or trait methods are considered as GraphQL fields. If a helper method is needed, or it should be ignored for some reason, then it should be marked with the #[graphql(ignore)]
attribute:
#![allow(dead_code)] extern crate juniper; use std::marker::PhantomPinned; use juniper::{graphql_interface, GraphQLInterface}; #[derive(GraphQLInterface)] struct Character { id: i32, #[graphql(ignore)] _pin: PhantomPinned, } #[graphql_interface] trait Person { fn name(&self) -> &str; fn age(&self) -> i32; #[graphql(ignore)] fn hidden_from_graphql(&self) { // Ignored methods are allowed to have a default implementation! } #[graphql(skip)] // ^^^^ alternative naming, up to your preference fn also_hidden_from_graphql(&self); } fn main() {}
TIP: See more available features in the API docs of the
#[graphql_interface]
attribute.