Skip to content

Commit

Permalink
Redesign #[graphql_interface] macro (#1009, #1000, #814)
Browse files Browse the repository at this point in the history
- remove support for `#[graphql_interface(dyn)]`
- describe all interface trait methods with type's fields or impl block instead of `#[graphql_interface]` attribute on `impl Trait`
- forbid default impls on non-skipped trait methods
- support additional nullable arguments on implementer
- support returning sub-type on implementer
  • Loading branch information
ilslv authored Jan 26, 2022
1 parent c866e09 commit 1aa1000
Show file tree
Hide file tree
Showing 82 changed files with 3,375 additions and 5,935 deletions.
219 changes: 18 additions & 201 deletions docs/book/content/types/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,14 @@ trait Character {
struct Human {
id: String,
}
#[graphql_interface] // implementing requires macro attribute too, (°o°)!
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}

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

# fn main() {
let human = Human { id: "human-32".to_owned() };
// Values type for interface has `From` implementations for all its implementers,
// so we don't need to bother with enum variant names.
let character: CharacterValue = human.into();
assert_eq!(character.id(), "human-32");
# }
#
# fn main() {}
```

Also, enum name can be specified explicitly, if desired.
Expand All @@ -90,71 +72,11 @@ struct Human {
id: String,
home_planet: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#
# fn main() {}
```


### Trait object values

If, for some reason, we would like to use [trait objects][2] for representing [interface][1] values incorporating dynamic dispatch, then it should be specified explicitly in the trait definition.

Downcasting [trait objects][2] in Rust is not that trivial, that's why macro transforms the trait definition slightly, imposing some additional type parameters under-the-hood.

> __NOTICE__:
> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object][2] to specify a [GraphQL interface][1] behind it.
```rust
# extern crate juniper;
# extern crate tokio;
use juniper::{graphql_interface, GraphQLObject};

// `dyn` argument accepts the name of type alias for the required trait object,
// and macro generates this alias automatically.
#[graphql_interface(dyn = DynCharacter, for = Human)]
trait Character {
async fn id(&self) -> &str; // async fields are supported natively
}

#[derive(GraphQLObject)]
#[graphql(impl = DynCharacter<__S>)] // macro adds `ScalarValue` type parameter to trait,
struct Human { // so it may be specified explicitly when required
id: String,
}
#[graphql_interface(dyn)] // implementing requires to know about dynamic dispatch too
impl Character for Human {
async fn id(&self) -> &str {
&self.id
}
}

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

# #[tokio::main]
# async fn main() {
let human = Human { id: "human-32".to_owned() };
let character: Box<DynCharacter> = Box::new(human);
assert_eq!(character.id().await, "human-32");
# }
```


### Ignoring trait methods

We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them.
Expand All @@ -176,12 +98,6 @@ trait Character {
struct Human {
id: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self) -> &str {
&self.id
}
}
#
# fn main() {}
```
Expand Down Expand Up @@ -278,24 +194,6 @@ struct Human {
id: String,
name: String,
}
#[graphql_interface]
impl Character for Human {
fn id(&self, db: &Database) -> Option<&str> {
if db.humans.contains_key(&self.id) {
Some(&self.id)
} else {
None
}
}

fn name(&self, db: &Database) -> Option<&str> {
if db.humans.contains_key(&self.id) {
Some(&self.name)
} else {
None
}
}
}
#
# fn main() {}
```
Expand All @@ -309,119 +207,50 @@ This requires to explicitly parametrize over [`ScalarValue`][3], as [`Executor`]

```rust
# extern crate juniper;
use juniper::{graphql_interface, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue};
use juniper::{graphql_interface, graphql_object, Executor, LookAheadMethods as _, ScalarValue};

#[graphql_interface(for = Human, Scalar = S)] // notice specifying `ScalarValue` as existing type parameter
trait Character<S: ScalarValue> {
// If a field argument is named `executor`, it's automatically assumed
// as an executor argument.
async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
where
S: Send + Sync; // required by `#[async_trait]` transformation ¯\_(ツ)_/¯
fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str;

// Otherwise, you may mark it explicitly as an executor argument.
async fn name<'b>(
fn name<'b>(
&'b self,
#[graphql(executor)] another: &Executor<'_, '_, (), S>,
) -> &'b str
where
S: Send + Sync;
) -> &'b str;

fn home_planet(&self) -> &str;
}

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

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


### Downcasting

By default, the [GraphQL interface][1] value is downcast to one of its implementer types via matching the enum variant or downcasting the trait object (if `dyn` macro argument is used).

However, if some custom logic is needed to downcast a [GraphQL interface][1] implementer, you may specify either an external function or a trait method to do so.

```rust
# extern crate juniper;
# use std::collections::HashMap;
use juniper::{graphql_interface, GraphQLObject};

struct Database {
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()` function
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

fn home_planet<'c, S>(&'c self, #[graphql(executor)] _: &Executor<'_, '_, (), S>) -> &'c str {
// Executor may not be present on the trait method ^^^^^^^^^^^^^^^^^^^^^^^^
&self.home_planet
}
}

// 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())
}
#
# fn main() {}
```

The attribute syntax `#[graphql_interface(on ImplementerType = resolver_fn)]` follows the [GraphQL syntax for downcasting interface implementer](https://spec.graphql.org/June2018/#example-5cc55).




Expand All @@ -445,25 +274,13 @@ 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
}
}
#
# fn main() {}
```
Expand Down
2 changes: 1 addition & 1 deletion examples/actix_subscriptions/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use actix_web::{

use juniper::{
graphql_object, graphql_subscription, graphql_value,
tests::fixtures::starwars::schema::{Character as _, Database, Query},
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, FieldError, RootNode,
};
use juniper_actix::{graphql_handler, playground_handler, subscriptions::subscriptions_handler};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use juniper::{graphql_interface, graphql_object};

pub struct ObjA {
id: String,
}

#[graphql_object(impl = CharacterValue)]
impl ObjA {
fn id(&self, is_present: bool) -> &str {
is_present.then(|| self.id.as_str()).unwrap_or("missing")
}
}

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

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
error[E0080]: evaluation of constant value failed
--> fail/interface/additional_non_nullable_argument.rs:14:1
|
14 | #[graphql_interface(for = ObjA)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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.', $DIR/fail/interface/additional_non_nullable_argument.rs:14:1
|
= note: this error originates in the macro `$crate::panic::panic_2015` (in Nightly builds, run with -Z macro-backtrace for more info)
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
use juniper::{graphql_interface, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
test: String,
}
use juniper::graphql_interface;

#[graphql_interface]
impl Character for ObjA {}

#[graphql_interface(for = ObjA)]
trait Character {
fn id(&self, __num: i32) -> &str {
"funA"
}
fn id(&self, __num: i32) -> &str;
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system.
--> fail/interface/argument_double_underscored.rs:14:18
|
14 | fn id(&self, __num: i32) -> &str {
| ^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

error[E0412]: cannot find type `CharacterValue` in this scope
--> fail/interface/argument_double_underscored.rs:4:18
--> fail/interface/argument_double_underscored.rs:5:18
|
4 | #[graphql(impl = CharacterValue)]
| ^^^^^^^^^^^^^^ not found in this scope

error[E0405]: cannot find trait `Character` in this scope
--> fail/interface/argument_double_underscored.rs:10:6
|
10 | impl Character for ObjA {}
| ^^^^^^^^^ not found in this scope
5 | fn id(&self, __num: i32) -> &str;
| ^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema
Loading

0 comments on commit 1aa1000

Please sign in to comment.