Attribute Macro juniper_codegen::graphql_interface[][src]

#[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.

The main 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 enum to represent a value type of GraphQL interface by default, however trait object may be used too (use dyn attribute argument for that).

A trait has to be object safe if its values are represented by trait object, because schema resolvers will need to return that trait object. The trait object has to be Send and Sync, and the macro automatically generate a convenien type alias for such trait object.

use juniper::{graphql_interface, GraphQLObject};

// NOTICE: By default a `CharacterValue` enum is generated by macro to represent values of this
//         GraphQL interface.
#[graphql_interface(for = [Human, Droid])] // enumerating all implementers is mandatory
trait Character {
    fn id(&self) -> &str;
}

// NOTICE: `dyn` attribute argument enables trait object usage to represent values of this
//         GraphQL interface. Also, for trait objects a trait is slightly modified
//         under-the-hood by adding a `ScalarValue` type parameter.
#[graphql_interface(dyn = DynSerial, for = Droid)]
trait Serial {
    fn number(&self) -> i32;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)] // notice the enum type name, not trait name
struct Human {
    id: String,
    home_planet: String,
}
#[graphql_interface]
impl Character for Human {
    fn id(&self) -> &str {
        &self.id
    }
}

#[derive(GraphQLObject)]
#[graphql(impl = [CharacterValue, DynSerial<__S>])] // notice the trait object referred by alias
struct Droid {                                      // and its parametrization by generic
    id: String,                                     // `ScalarValue`
    primary_function: String,
}
#[graphql_interface]
impl Character for Droid {
    fn id(&self) -> &str {
        &self.id
    }
}
#[graphql_interface(dyn)] // implementing requires to know about dynamic dispatch too
impl Serial for Droid {
    fn number(&self) -> i32 {
        78953
    }
}

Custom name, description, deprecation and argument defaults

The name of GraphQL interface, its field, or a field argument may be overriden 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 regulat 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(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;
}

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/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 omited 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(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>;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Human {
    id: String,
    home_planet: String,
}
#[graphql_interface]
impl Character for Human {
    fn id<'db>(&self, db: &'db Database) -> Option<&'db str> {
        db.humans.get(&self.id).map(|h| h.id.as_str())
    }
    fn info<'db>(&self, db: &'db Database) -> Option<&'db str> {
        db.humans.get(&self.id).map(|h| h.home_planet.as_str())
    }
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Droid {
    id: String,
    primary_function: String,
}
#[graphql_interface]
impl Character for Droid {
    fn id<'db>(&self, db: &'db Database) -> Option<&'db str> {
        db.droids.get(&self.id).map(|h| h.id.as_str())
    }
    fn info<'db>(&self, db: &'db Database) -> Option<&'db str> {
        db.droids.get(&self.id).map(|h| h.primary_function.as_str())
    }
}

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 omited in GraphQL schema.

However, this requires to explicitly parametrize over ScalarValue, as Executor does so.

// NOTICE: Specifying `ScalarValue` as existing type parameter.
#[graphql_interface(for = Human, Scalar = S)]
trait Character<S: ScalarValue> {
    async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
    where
        S: Send + Sync; // required by `#[async_trait]` transformation ¯\_(ツ)_/¯

    async fn name<'b>(
        &'b self,
        #[graphql(executor)] another: &Executor<'_, '_, (), S>,
    ) -> &'b str
    where
        S: Send + Sync;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue<__S>)]
struct Human {
    id: String,
    name: String,
}
#[graphql_interface(Scalar = S)]
impl<S: ScalarValue> Character<S> for Human {
    async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
    where
        S: Send + Sync,
    {
        executor.look_ahead().field_name()
    }

    async fn name<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str
    where
        S: Send + Sync,
    {
        &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/Scalar/ScalarValue attribute’s argument.

// NOTICE: Removing `Scalar` argument will fail compilation.
#[graphql_interface(for = [Human, Droid], Scalar = DefaultScalarValue)]
trait Character {
    fn id(&self) -> &str;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
struct Human {
    id: String,
    home_planet: String,
}
#[graphql_interface(Scalar = DefaultScalarValue)]
impl Character for Human {
    fn id(&self) -> &str{
        &self.id
    }
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
struct Droid {
    id: String,
    primary_function: String,
}
#[graphql_interface(Scalar = DefaultScalarValue)]
impl Character for Droid {
    fn id(&self) -> &str {
        &self.id
    }
}

Ignoring trait methods

To omit some trait method to be assumed as a GraphQL interface field and ignore it, use an ignore/skip attribute’s argument directly on that method.

#[graphql_interface]
trait Character {
    fn id(&self) -> &str;

    #[graphql(ignore)]  // or `#[graphql(skip)]`, your choice
    fn kaboom(&mut self);
}

Downcasting

By default, the GraphQL interface value is downcast to one of its implementer types via matching the enum variant or downcasting the trait object (if dyn attribute’s argument is used).

To use a custom logic for downcasting a GraphQL interface into its implementer, there may be specified:

  • either a downcast attribute’s argument directly on a trait method;
  • or an on attribute’s argument on aa trait definition referring an exteranl function.
struct Database {
    humans: HashMap<String, Human>,
    droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}

#[graphql_interface(for = [Human, Droid], Context = Database)]
#[graphql_interface(on Droid = get_droid)] // enables downcasting `Droid` via `get_droid()`
trait Character {
    fn id(&self) -> &str;

    #[graphql(downcast)] // makes method a downcast to `Human`, not a field
    // NOTICE: The method signature may optionally contain `&Database` context argument.
    fn as_human(&self) -> Option<&Human> {
        None
    }
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Human {
    id: String,
}
#[graphql_interface]
impl Character for Human {
    fn id(&self) -> &str {
        &self.id
    }

    fn as_human(&self) -> Option<&Self> {
        Some(self)
    }
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Droid {
    id: String,
}
#[graphql_interface]
impl Character for Droid {
    fn id(&self) -> &str {
        &self.id
    }
}

// External downcast function doesn't have to be a method of a type.
// It's only a matter of the function signature to match the requirements.
fn get_droid<'db>(ch: &CharacterValue, db: &'db Database) -> Option<&'db Droid> {
    db.droids.get(ch.id())
}