Part of #295
Fixes #544, #605, #658, #748
Related to #421, #2
...or not so great? 🤔
Motivation
We want GraphQL interfaces which just feel right like usual ones in many PLs.
~~They should be able to work with an open set of types without requiring a user to specify all the concrete types an interface can downcasts into on the interface side.~~
We're unable to do that, at the moment 😕
But we still could have a nice proc macros as convenient as they can be 🙃
New design
Traits
#[graphql_interface(enum = CharacterValue)] // <-- use enum for dispatching
#[graphql_interface(for = [Human, Droid, Jedi])] // <-- mandatory =( and should enumerate all implementors
#[graphql_interface(name = "Character", desc = "Traited character")]
#[graphql_interface(context = MyContext)] // <-- if not specified, then auto-inferred from methods, if possible, otherwise is `()`
#[graphql_interface(scalar = juniper::DefaultScalarValue)] // <-- if not specified, then generated code is parametrized by `__S: ScalarValue`
#[graphql_interface(on Jedi = custom_downcast_as_jedi)] // <-- custom external downcaster
trait Character {
fn id(&self) -> &str;
// Optional custom downcaster.
#[graphql_interface(downcast)]
fn as_droid(&self) -> Option<&Droid> { None }
}
#[graphql_interface(dyn = DynCharacter2, for = Human)] // use trait object for dispatching
trait Character2 {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(implements = [CharacterValue, DynCharacter2], Context = MyContext)]
struct Human {
id: String,
home_planet: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#[graphql_interface(dyn)] // <-- this `dyn` is inevitable when dispatching via trait objects =(
impl Character2 for Human {
fn id(&self) -> &str {
&self.id
}
}
#[derive(GraphQLObject)]
#[graphql(implements = CharacterValue, Context = MyContext)]
struct Droid {
id: String,
primary_function: String,
}
#[graphql_interface]
impl Character for Droid {
fn id(&self) -> &str {
&self.id
}
fn as_droid(&self) -> Option<&Droid> {
Some(self)
}
}
#[derive(GraphQLObject)]
#[graphql(implements = CharacterValue, Context = MyContext)]
struct Jedi {
id: String,
rank: String,
}
#[graphql_interface]
impl Character for Jedi {
fn id(&self) -> &str {
&self.id
}
}
fn custom_downcast_as_jedi(ch: &CharacterValue, ctx: &MyContext) -> Option<&Jedi> {
ctx.can_i_haz_the_jedi_please(ch)
}
The nearest analogue of GraphQL interfaces are, definitely, traits. However, they are a little different beasts than may seem at first glance. The main difference goes that in GraphQL interface type serves both as an abstraction and a value dispatching to concrete implementers, while in Rust, a trait is an abstraction only and you need a separate type to dispatch to a concrete implemeter, like enum or trait object, because trait is not even a type itself. This difference imposes a lot of unobvious corner cases when we try to express GraphQL interfaces in Rust. This PR tries to implement a reasonable middleground.
So, poking with GraphQL interfaces will require two parts:
- declare the abstraction;
- operate ingerface's values.
Declaring an abstraction is almost as simple as declaring trait:
#[graphql_interface(enum = CharacterValue)] // <-- specify type ident to represent value of interface
#[graphql_interface(for = [Human, Droid, Jedi])] // <-- specify implementers
trait Character {
fn id(&self) -> &str;
}
And then, you need to operate in a code with a Rust implementing this GraphQL interface's value:
#[derive(GraphQLObject)]
#[graphql(implements = CharacterValue)] // <-- we refer here to Rust type implementing GraphQL interface, not trait
struct Human {
id: String,
}
#[graphql_interface]
impl Character for Human { <-- and here we're implementing GraphQL interface for Human Graphql object as a regulat trait impl
fn id(&self) -> &str {
&self.id
}
}
Trait objects
By default, an enum dispatch is used, because we should enumerate all the implementers anyway.
However, a dyn dispatch via trait is also possible to be used. As this requires a llitle bit more code transformation and aiding during macros expansion, we shoud specify it explicitly both on the interface and the implementers:
#[graphql_interface(dyn = DynCharacter)] // <-- generates handy type alias for the trait object
#[graphql_interface(for = [Human, Droid, Jedi])] // <-- specify implementers
trait Character {
fn id(&self) -> &str;
}
#[derive(GraphQLObject)]
#[graphql(implements = dyn CharacterValue)] // <-- `dyn` part is vital here, as an underlying trait is transformed
struct Human {
id: String,
}
#[graphql_interface(dyn)] // <-- `dyn` part is vital here, as an underlying trait is transformed
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
Checklist
- [x]
derive(GraphQLObject)
/graphql_object
macros:
- [x] support
implements
attribute's argument to enumerate all traits this object implements.
- [x] Traits (
#[graphql_interface(dyn)]
):
- [x]
trait
transformation:
- [x] support multiple attributes and remove duplicated attributes on first expansion;
- [x] transform trait to extend
AsGraphQLValue
in a object-safe manner;
- [x] ensure trait is object safe;
- [x] enumerate implementors with
#[graphql_interface(for = [Type1, Type2])]
attribute;
- [x] ensure types are unique on type level;
- [x] ensure types are GraphQL objects;
- [x] support custom GraphQL name and description with
#[graphql_interface(name = "Type")]
and #[graphql_interface(description = "A Type desc.")]
attributes;
- [x] support custom GraphQL field deprecation with
#[graphql_interface(deprecated = "Text")]
attributes;
- [x] support custom context type with
#[graphql_interface(context = Type)]
attribute;
- [x] support custom names for
Context
argument and allow to mark it with #[graphq_interface(context)]
attribute;
- [x] support accepting executor in method arguments, optionally marked with
#[graphq_interface(executor)]
attribute;
- [x] support custom
ScalarValue
type #[graphql_interface(scalar = Type)]
attribute;
- [x] allow explicit custom
ScalarValue
type patameter;
- [x] ignore trait methods with
#[graphql_interface(ignore)]
attribute;
- [x] support downcasting methods with
#[graphql_interface(downcast)]
attribute;
- [x] allow omitting
Context
in methods;
- [x] work correctly with parenthesized types;
- [x] allow full import paths as types;
- [x] validate signature for good error messages.
- [x] support external downcasters on trait itself with
#[graphql_interface(on Type = downcast_fn)]
attribute;
- [x] ~~support trivial deriving via
#[graphql_interface(derive(Type))]
attribute.~~ (will be implemented in a separate PR)
- [x]
impl
transformation:
- [x] ~~support custom context type with
#[graphql_interface(context = Type)]
attribute;~~ (not required)
- [x] support custom
ScalarValue
type #[graphql_interface(scalar = Type)]
attribute;
- [x] allow explicit custom
ScalarValue
type patameter.
- [x] Enums (
#[graphql_interface(enum)]
):
- [x] generate
enum
type of all implementers;
- [x] generate
From
impls to this enum
for each implementer;
- [x] impl original trait by this
enum
.
enhancement docs breaking-change api