#[graphql_interface]
Expand description
#[graphql_interface]
macro for generating a GraphQL interface
implementation for traits and its implementers.
Specifying multiple #[graphql_interface]
attributes on the same definition
is totally okay. They all will be treated as a single attribute.
GraphQL interfaces are more like structurally-typed interfaces, while
Rust’s traits are more like type classes. Using impl Trait
isn’t an
option, so you have to cover all trait’s methods with type’s fields or
impl block.
Another difference between GraphQL interface type and Rust trait is that the former serves both as an abstraction and a value downcastable to concrete implementers, while in Rust, a trait is an abstraction only and you need a separate type to downcast into a concrete implementer, like enum or trait object, because trait doesn’t represent a type itself. Macro uses Rust enums only to represent a value type of a GraphQL interface.
GraphQL interface can be represented with struct in case methods don’t have any arguments:
use juniper::{graphql_interface, GraphQLObject};
// NOTICE: By default a `CharacterValue` enum is generated by macro to represent values of this
// GraphQL interface.
#[graphql_interface]
#[graphql(for = Human)] // enumerating all implementers is mandatory
struct Character {
id: String,
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)] // notice the enum type name, not trait name
struct Human {
id: String, // this field is used to resolve Character::id
home_planet: String,
}
Also GraphQL interface can be represented with trait:
use juniper::{graphql_interface, GraphQLObject};
// NOTICE: By default a `CharacterValue` enum is generated by macro to represent values of this
// GraphQL interface.
#[graphql_interface]
#[graphql(for = Human)] // enumerating all implementers is mandatory
trait Character {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)] // notice the enum type name, not trait name
struct Human {
id: String, // this field is used to resolve Character::id
home_planet: String,
}
NOTE: Struct or trait representing interface acts only as a blueprint for names of methods, their arguments and return type, so isn’t actually used at a runtime. But no-one is stopping you from implementing trait manually for your own usage.
§Custom name, description, deprecation and argument defaults
The name of GraphQL interface, its field, or a field argument may be overridden with a
name
attribute’s argument. By default, a type name is used or camelCased
method/argument
name.
The description of GraphQL interface, its field, or a field argument may be specified
either with a description
/desc
attribute’s argument, or with a regular Rust doc comment.
A field of GraphQL interface may be deprecated by specifying a deprecated
attribute’s
argument, or with regular Rust #[deprecated]
attribute.
The default value of a field argument may be specified with a default
attribute argument (if
no exact value is specified then [Default::default
] is used).
#[graphql_interface]
#[graphql(name = "Character", desc = "Possible episode characters.")]
trait Chrctr {
#[graphql(name = "id", desc = "ID of the character.")]
#[graphql(deprecated = "Don't use it")]
fn some_id(
&self,
#[graphql(name = "number", desc = "Arbitrary number.")]
#[graphql(default = 5)]
num: i32,
) -> &str;
}
// NOTICE: Rust docs are used as GraphQL description.
/// Possible episode characters.
#[graphql_interface]
trait CharacterWithDocs {
/// ID of the character.
#[deprecated]
fn id(&self, #[graphql(default)] num: i32) -> &str;
}
§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 =
orimpl =
attributes is an understandable compile-time error.
use juniper::{graphql_interface, graphql_object, ID};
#[graphql_interface]
#[graphql(for = [HumanValue, Luke])]
struct Node {
id: ID,
}
#[graphql_interface]
#[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, 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 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 fields, which aren’t
present on an original interface.
use juniper::{graphql_interface, graphql_object, ID};
#[graphql_interface]
#[graphql(for = [HumanValue, Luke])]
struct Node {
id: ID,
}
#[graphql_interface]
#[graphql(for = HumanConnectionValue)]
struct Connection {
nodes: Vec<NodeValue>,
}
#[graphql_interface]
#[graphql(impl = NodeValue, for = Luke)]
struct Human {
id: ID,
home_planet: String,
}
#[graphql_interface]
#[graphql(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]
#[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, 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!(),
}
}
}
§Renaming policy
By default, all GraphQL interface fields and their arguments are renamed
via camelCase
policy (so fn my_id(&self) -> String
becomes myId
field
in GraphQL schema, and so on). This complies with default GraphQL naming
conventions demonstrated in spec.
However, if you need for some reason apply another naming convention, it’s
possible to do by using rename_all
attribute’s argument. At the moment it
supports the following policies only: SCREAMING_SNAKE_CASE
, camelCase
,
none
(disables any renaming).
#[graphql_interface]
#[graphql(for = Human, rename_all = "none")] // disables renaming
trait Character {
// NOTICE: In the generated GraphQL schema this field and its argument
// will be `detailed_info` and `info_kind`.
fn detailed_info(&self, info_kind: String) -> String;
}
struct Human {
id: String,
home_planet: String,
}
#[graphql_object]
#[graphql(impl = CharacterValue, rename_all = "none")]
impl Human {
fn id(&self) -> &str {
&self.id
}
fn home_planet(&self) -> &str {
&self.home_planet
}
// You can return `&str` even if trait definition returns `String`.
fn detailed_info(&self, info_kind: String) -> &str {
(info_kind == "planet")
.then_some(&self.home_planet)
.unwrap_or(&self.id)
}
}
§Ignoring trait methods
To omit some trait method to be assumed as a GraphQL interface field
and ignore it, use an ignore
attribute’s argument directly on that method.
#[graphql_interface]
trait Character {
fn id(&self) -> &str;
#[graphql(ignore)]
fn kaboom(&mut self);
}
§Custom context
By default, the generated implementation tries to infer Context
type from signatures of
trait methods, and uses unit type ()
if signatures contains no Context
arguments.
If Context
type cannot be inferred or is inferred incorrectly, then specify it explicitly
with context
attribute’s argument.
If trait method represents a GraphQL interface field and its argument is named as context
or ctx
then this argument is assumed as Context
and will be omitted in GraphQL schema.
Additionally, any argument may be marked as Context
with a context
attribute’s argument.
struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}
#[graphql_interface]
#[graphql(for = [Human, Droid], Context = Database)]
trait Character {
fn id<'db>(&self, ctx: &'db Database) -> Option<&'db str>;
fn info<'db>(&self, #[graphql(context)] db: &'db Database) -> Option<&'db str>;
}
struct Human {
id: String,
home_planet: String,
}
#[graphql_object]
#[graphql(impl = CharacterValue, Context = Database)]
impl Human {
fn id<'db>(&self, context: &'db Database) -> Option<&'db str> {
context.humans.get(&self.id).map(|h| h.id.as_str())
}
fn info<'db>(&self, #[graphql(context)] db: &'db Database) -> Option<&'db str> {
db.humans.get(&self.id).map(|h| h.home_planet.as_str())
}
fn home_planet(&self) -> &str {
&self.home_planet
}
}
struct Droid {
id: String,
primary_function: String,
}
#[graphql_object]
#[graphql(impl = CharacterValue, Context = Database)]
impl Droid {
fn id<'db>(&self, ctx: &'db Database) -> Option<&'db str> {
ctx.droids.get(&self.id).map(|h| h.id.as_str())
}
fn info<'db>(&self, #[graphql(context)] db: &'db Database) -> Option<&'db str> {
db.droids.get(&self.id).map(|h| h.primary_function.as_str())
}
fn primary_function(&self) -> &str {
&self.primary_function
}
}
§Using Executor
If an Executor
is required in a trait method to resolve a GraphQL interface field,
specify it as an argument named as executor
or explicitly marked with an executor
attribute’s argument. Such method argument will be omitted in GraphQL schema.
However, this requires to explicitly parametrize over ScalarValue
, as Executor
does so.
#[graphql_interface]
// NOTICE: Specifying `ScalarValue` as existing type parameter.
#[graphql(for = Human, scalar = S)]
trait Character<S: ScalarValue> {
fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str;
fn name<'b>(
&'b self,
#[graphql(executor)] another: &Executor<'_, '_, (), S>,
) -> &'b str;
}
struct Human {
id: String,
name: String,
}
#[graphql_object]
#[graphql(scalar = S: ScalarValue, impl = CharacterValue<S>)]
impl Human {
async fn id<'a, S>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
where
S: ScalarValue,
{
executor.look_ahead().field_name()
}
async fn name<'b, S>(&'b self, _executor: &Executor<'_, '_, (), S>) -> &'b str {
&self.name
}
}
§Custom ScalarValue
By default, #[graphql_interface]
macro generates code, which is generic
over a ScalarValue
type. This may introduce a problem when at least one
of GraphQL interface implementers is restricted to a concrete
ScalarValue
type in its implementation. To resolve such problem, a
concrete ScalarValue
type should be specified with a scalar
attribute’s argument.
#[graphql_interface]
// NOTICE: Removing `Scalar` argument will fail compilation.
#[graphql(for = Human, scalar = DefaultScalarValue)]
trait Character {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, scalar = DefaultScalarValue)]
struct Human {
id: String,
home_planet: String,
}