From b1a03661122c0fa5c8a4fc41418dee7f9ede6308 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 6 Oct 2020 11:58:30 +0200 Subject: [PATCH] Fix interfaces description in Book (#682, #658) --- docs/book/content/types/interfaces.md | 314 ++++++++++++-------------- 1 file changed, 144 insertions(+), 170 deletions(-) diff --git a/docs/book/content/types/interfaces.md b/docs/book/content/types/interfaces.md index 921cdb9f5..1750b2e04 100644 --- a/docs/book/content/types/interfaces.md +++ b/docs/book/content/types/interfaces.md @@ -1,9 +1,9 @@ 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; 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 { fn id(&self) -> &str; } @@ -51,7 +51,19 @@ impl Character for Human { &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, @@ -91,7 +103,7 @@ impl Character for Human { ### 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. @@ -104,7 +116,7 @@ Downcasting [trait objects][2] in Rust is not that trivial, that's why macro tra use juniper::{graphql_interface, GraphQLObject}; // `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)] trait Character { async fn id(&self) -> &str; // async fields are supported natively @@ -121,7 +133,19 @@ impl Character for Human { &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() }; @@ -152,7 +176,7 @@ trait Character { struct Human { id: String, } -#[graphql_interface] // implementing requires macro attribute too, (°o°)! +#[graphql_interface] impl Character for Human { fn id(&self) -> &str { &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 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 Character 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. - -Traits are maybe the most obvious concept you want to use when building -interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll -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, -} - -trait Character { - fn id(&self) -> &str; - - // Downcast methods, each concrete class will need to implement one of these - fn as_human(&self) -> Option<&Human> { None } - fn as_droid(&self) -> Option<&Droid> { 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 = |&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 - &Human => self.as_human(), - &Droid => self.as_droid(), - } -}); - -# 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, -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 +```rust +# extern crate juniper; # 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, -} +use juniper::{graphql_interface, GraphQLObject}; struct Database { - humans: HashMap, droids: HashMap, } - 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; -} -impl Character for Human { - fn id(&self) -> &str { self.id.as_str() } + #[graphql_interface(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 + } } -impl Character for Droid { - fn id(&self) -> &str { self.id.as_str() } +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue, Context = Database)] +struct Human { + id: String, } - -juniper::graphql_interface!(<'a> &'a dyn Character: Database as "Character" where Scalar = |&self| { - field id() -> &str { self.id() } - - instance_resolvers: |&context| { - &Human => context.humans.get(self.id()), - &Droid => context.droids.get(self.id()), +#[graphql_interface] +impl Character for Human { + fn id(&self) -> &str { + &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, + fn as_human(&self) -> Option<&Self> { + Some(self) + } } -#[derive(juniper::GraphQLObject)] -#[graphql(Context = "Database")] +#[derive(GraphQLObject)] +#[graphql(impl = CharacterValue, Context = Database)] struct Droid { id: String, - primary_function: String, } - -struct Database { - humans: HashMap, - droids: HashMap, +#[graphql_interface] +impl Character for Droid { + fn id(&self) -> &str { + &self.id + } } -impl juniper::Context for Database {} - -struct Character { - id: String, +// 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() {} +``` -juniper::graphql_interface!(Character: Database where Scalar = |&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. +## `ScalarValue` considerations -## Enums +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. -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 +# extern crate juniper; +use juniper::{graphql_interface, DefaultScalarValue, GraphQLObject}; -```rust,ignore -#[derive(juniper::GraphQLObject)] +#[graphql_interface(for = [Human, Droid])] +#[graphql_interface(Scalar = DefaultScalarValue)] // removing this line will fail compilation +trait Character { + fn id(&self) -> &str; +} + +#[derive(GraphQLObject)] +#[graphql(Scalar = DefaultScalarValue)] struct Human { id: String, home_planet: String, } +#[graphql_interface(Scalar = DefaultScalarValue)] +impl Character for Human { + fn id(&self) -> &str { + &self.id + } +} -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] struct Droid { id: String, primary_function: String, } - -# #[allow(dead_code)] -enum Character { - Human(Human), - Droid(Droid), +#[graphql_interface(Scalar = DefaultScalarValue)] +impl Character for Droid { + fn id(&self) -> &str { + &self.id + } } - -juniper::graphql_interface!(Character: () where Scalar = |&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() {} ``` @@ -485,4 +458,5 @@ juniper::graphql_interface!(Character: () where Scalar = |&self| { [1]: https://spec.graphql.org/June2018/#sec-Interfaces [2]: https://doc.rust-lang.org/reference/types/trait-object.html [3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html -[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html \ No newline at end of file +[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html +[5]: https://spec.graphql.org/June2018/#sec-Objects