Fix interfaces description in Book (#682, #658)

This commit is contained in:
tyranron 2020-10-06 11:58:30 +02:00
parent cbf16c5a33
commit b1a0366112
No known key found for this signature in database
GPG key ID: 762E144FB230A4F0

View file

@ -1,9 +1,9 @@
Interfaces Interfaces
========== ==========
[GraphQL interfaces][1] map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][1] are Rust traits, and the main difference is that in GraphQL [interface type][1] serves both as an _abstraction_ and a _boxed value (downcastable to concrete implementers)_, while in Rust, a trait is an _abstraction only_ and _to represent such a boxed value a separate type is required_, like enum or trait object, because Rust trait does not represent a type itself, and so can have no values. This difference imposes some unintuitive and non-obvious corner cases when we try to express [GraphQL interfaces][1] in Rust, but on the other hand gives you full control over which type is backing your interface, and how it's resolved. [GraphQL interfaces][1] map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][1] are Rust traits, and the main difference is that in GraphQL an [interface type][1] serves both as an _abstraction_ and a _boxed value (downcastable to concrete implementers)_, while in Rust, a trait is an _abstraction only_ and _to represent such a boxed value a separate type is required_, like enum or trait object, because Rust trait doesn't represent a type itself, and so can have no values. This difference imposes some unintuitive and non-obvious corner cases when we try to express [GraphQL interfaces][1] in Rust, but on the other hand gives you full control over which type is backing your interface, and how it's resolved.
For implementing [GraphQL interfaces][1] Juniper provides `#[graphql_interface]` macro. For implementing [GraphQL interfaces][1] Juniper provides the `#[graphql_interface]` macro.
@ -35,7 +35,7 @@ By default, Juniper generates an enum representing the values of the defined [Gr
# extern crate juniper; # extern crate juniper;
use juniper::{graphql_interface, GraphQLObject}; use juniper::{graphql_interface, GraphQLObject};
#[graphql_interface(for = Human)] // enumerating all implementers is mandatory #[graphql_interface(for = [Human, Droid])] // enumerating all implementers is mandatory
trait Character { trait Character {
fn id(&self) -> &str; fn id(&self) -> &str;
} }
@ -51,7 +51,19 @@ impl Character for Human {
&self.id &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() { # fn main() {
let human = Human { id: "human-32".to_owned() }; let human = Human { id: "human-32".to_owned() };
// Values type for interface has `From` implementations for all its implementers, // Values type for interface has `From` implementations for all its implementers,
@ -91,7 +103,7 @@ impl Character for Human {
### Trait object values ### Trait object values
If, for some reason, we would like to use [trait objects][2] for representing [interface][1] values incorporating dynamic dispatch, that should be specified explicitly in the trait definition. 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. 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.
@ -104,7 +116,7 @@ Downcasting [trait objects][2] in Rust is not that trivial, that's why macro tra
use juniper::{graphql_interface, GraphQLObject}; use juniper::{graphql_interface, GraphQLObject};
// `dyn` argument accepts the name of type alias for the required trait object, // `dyn` argument accepts the name of type alias for the required trait object,
// and macro generates this alias automatically // and macro generates this alias automatically.
#[graphql_interface(dyn = DynCharacter, for = Human)] #[graphql_interface(dyn = DynCharacter, for = Human)]
trait Character { trait Character {
async fn id(&self) -> &str; // async fields are supported natively async fn id(&self) -> &str; // async fields are supported natively
@ -121,7 +133,19 @@ impl Character for Human {
&self.id &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] # #[tokio::main]
# async fn main() { # async fn main() {
let human = Human { id: "human-32".to_owned() }; let human = Human { id: "human-32".to_owned() };
@ -152,7 +176,7 @@ trait Character {
struct Human { struct Human {
id: String, id: String,
} }
#[graphql_interface] // implementing requires macro attribute too, (°o°)! #[graphql_interface]
impl Character for Human { impl Character for Human {
fn id(&self) -> &str { fn id(&self) -> &str {
&self.id &self.id
@ -163,6 +187,52 @@ impl Character for Human {
``` ```
### Fields, arguments and interface customization
Similarly to [GraphQL objects][5] Juniper allows to fully customize [interface][1] fields and their arguments.
```rust
# #![allow(deprecated)]
# extern crate juniper;
use juniper::graphql_interface;
// Renames the interface in GraphQL schema.
#[graphql_interface(name = "MyCharacter")]
// Describes the interface in GraphQL schema.
#[graphql_interface(description = "My own character.")]
// Usual Rust docs are supported too as GraphQL interface description,
// but `description` attribute argument takes precedence over them, if specified.
/// This doc is absent in GraphQL schema.
trait Character {
// Renames the field in GraphQL schema.
#[graphql_interface(name = "myId")]
// Deprecates the field in GraphQL schema.
// Usual Rust `#[deprecated]` attribute is supported too as field deprecation,
// but `deprecated` attribute argument takes precedence over it, if specified.
#[graphql_interface(deprecated = "Do not use it.")]
// Describes the field in GraphQL schema.
#[graphql_interface(description = "ID of my own character.")]
// Usual Rust docs are supported too as field description,
// but `description` attribute argument takes precedence over them, if specified.
/// This description is absent in GraphQL schema.
fn id(
&self,
// Renames the argument in GraphQL schema.
#[graphql_interface(name = "myNum")]
// Describes the argument in GraphQL schema.
#[graphql_interface(description = "ID number of my own character.")]
// Specifies the default value for the argument.
// The concrete value may be omitted, and the `Default::default` one
// will be used in such case.
#[graphql_interface(default = 5)]
num: i32,
) -> &str;
}
#
# fn main() {}
```
### Custom context ### Custom context
If a context is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument. If a context is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument.
@ -271,210 +341,113 @@ impl<S: ScalarValue> Character<S> for Human {
``` ```
### 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` 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};
Traits are maybe the most obvious concept you want to use when building struct Database {
interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll droids: HashMap<String, Droid>,
have to manually specify how to convert a trait into a concrete type. This can
be done in a couple of different ways:
### Downcasting via accessor methods
```rust,ignore
#[derive(juniper::GraphQLObject)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
struct Droid {
id: String,
primary_function: String,
} }
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 { trait Character {
fn id(&self) -> &str; fn id(&self) -> &str;
// Downcast methods, each concrete class will need to implement one of these #[graphql_interface(downcast)] // makes method a downcast to `Human`, not a field
fn as_human(&self) -> Option<&Human> { None } // NOTICE: The method signature may optionally contain `&Database` context argument.
fn as_droid(&self) -> Option<&Droid> { None } fn as_human(&self) -> Option<&Human> {
} None
impl Character for Human {
fn id(&self) -> &str { self.id.as_str() }
fn as_human(&self) -> Option<&Human> { Some(&self) }
}
impl Character for Droid {
fn id(&self) -> &str { self.id.as_str() }
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
}
juniper::graphql_interface!(<'a> &'a dyn Character: () as "Character" where Scalar = <S> |&self| {
field id() -> &str { self.id() }
instance_resolvers: |_| {
// The left hand side indicates the concrete type T, the right hand
// side should be an expression returning Option<T>
&Human => self.as_human(),
&Droid => self.as_droid(),
} }
}); }
#[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())
}
#
# fn main() {} # fn main() {}
``` ```
The `instance_resolvers` declaration lists all the implementers of the given
interface and how to resolve them.
As you can see, you lose a bit of the point with using traits: you need to list
all the concrete types in the trait itself, and there's a bit of repetition
going on.
### Using an extra database lookup
If you can afford an extra database lookup when the concrete class is requested, ## `ScalarValue` considerations
you can do away with the downcast methods and use the context instead. Here,
we'll use two hashmaps, but this could be two tables and some SQL calls instead:
```rust,ignore By default, `#[graphql_interface]` macro generates code, which is generic over a [`ScalarValue`][3] type. This may introduce a problem when at least one of [GraphQL interface][1] implementers is restricted to a concrete [`ScalarValue`][3] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][3] type should be specified.
# use std::collections::HashMap;
#[derive(juniper::GraphQLObject)]
#[graphql(Context = Database)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)] ```rust
#[graphql(Context = Database)] # extern crate juniper;
struct Droid { use juniper::{graphql_interface, DefaultScalarValue, GraphQLObject};
id: String,
primary_function: String,
}
struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}
#[graphql_interface(for = [Human, Droid])]
#[graphql_interface(Scalar = DefaultScalarValue)] // removing this line will fail compilation
trait Character { trait Character {
fn id(&self) -> &str; fn id(&self) -> &str;
} }
#[derive(GraphQLObject)]
#[graphql(Scalar = DefaultScalarValue)]
struct Human {
id: String,
home_planet: String,
}
#[graphql_interface(Scalar = DefaultScalarValue)]
impl Character for Human { impl Character for Human {
fn id(&self) -> &str { self.id.as_str() } fn id(&self) -> &str {
&self.id
}
} }
#[derive(GraphQLObject)]
struct Droid {
id: String,
primary_function: String,
}
#[graphql_interface(Scalar = DefaultScalarValue)]
impl Character for Droid { impl Character for Droid {
fn id(&self) -> &str { self.id.as_str() } fn id(&self) -> &str {
&self.id
}
} }
#
juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" where Scalar = <S> |&self| {
field id() -> &str { self.id() }
instance_resolvers: |&context| {
&Human => context.humans.get(self.id()),
&Droid => context.droids.get(self.id()),
}
});
# fn main() {}
```
This removes the need of downcast methods, but still requires some repetition.
## Placeholder objects
Continuing on from the last example, the trait itself seems a bit unneccesary.
Maybe it can just be a struct containing the ID?
```rust,ignore
# use std::collections::HashMap;
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
struct Droid {
id: String,
primary_function: String,
}
struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}
struct Character {
id: String,
}
juniper::graphql_interface!(Character: Database where Scalar = <S> |&self| {
field id() -> &str { self.id.as_str() }
instance_resolvers: |&context| {
&Human => context.humans.get(&self.id),
&Droid => context.droids.get(&self.id),
}
});
# fn main() {}
```
This reduces repetition some more, but might be impractical if the interface's
surface area is large.
## Enums
Using enums and pattern matching lies half-way between using traits and using
placeholder objects. We don't need the extra database call in this case, so
we'll remove it.
```rust,ignore
#[derive(juniper::GraphQLObject)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
struct Droid {
id: String,
primary_function: String,
}
# #[allow(dead_code)]
enum Character {
Human(Human),
Droid(Droid),
}
juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
field id() -> &str {
match *self {
Character::Human(Human { ref id, .. }) |
Character::Droid(Droid { ref id, .. }) => id,
}
}
instance_resolvers: |_| {
&Human => match *self { Character::Human(ref h) => Some(h), _ => None },
&Droid => match *self { Character::Droid(ref d) => Some(d), _ => None },
}
});
# fn main() {} # fn main() {}
``` ```
@ -485,4 +458,5 @@ juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
[1]: https://spec.graphql.org/June2018/#sec-Interfaces [1]: https://spec.graphql.org/June2018/#sec-Interfaces
[2]: https://doc.rust-lang.org/reference/types/trait-object.html [2]: https://doc.rust-lang.org/reference/types/trait-object.html
[3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html [3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html [4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html
[5]: https://spec.graphql.org/June2018/#sec-Objects