Attribute Macro juniper_codegen::graphql_interface

source ·
#[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 = or impl = 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 nullable 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 nullable 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,
}