|
1 |
| -# Interfaces |
| 1 | +Interfaces |
| 2 | +========== |
| 3 | + |
| 4 | +[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. |
| 5 | + |
| 6 | +For implementing [GraphQL interfaces][1] Juniper provides `#[graphql_interface]` macro. |
| 7 | + |
2 | 8 |
|
3 |
| -GraphQL interfaces map well to interfaces known from common object-oriented |
4 |
| -languages such as Java or C#, but Rust has unfortunately not a concept that maps |
5 |
| -perfectly to them. Because of this, defining interfaces in Juniper can require a |
6 |
| -little bit of boilerplate code, but on the other hand gives you full control |
7 |
| -over which type is backing your interface. |
8 | 9 |
|
9 |
| -To highlight a couple of different ways you can implement interfaces in Rust, |
10 |
| -let's have a look at the same end-result from a few different implementations: |
11 | 10 |
|
12 | 11 | ## Traits
|
13 | 12 |
|
| 13 | +Defining a trait is mandatory for defining a [GraphQL interface][1], because this is the _obvious_ way we describe an _abstraction_ in Rust. All [interface][1] fields are defined as computed ones via trait methods. |
| 14 | + |
| 15 | +```rust |
| 16 | +# extern crate juniper; |
| 17 | +use juniper::graphql_interface; |
| 18 | + |
| 19 | +#[graphql_interface] |
| 20 | +trait Character { |
| 21 | + fn id(&self) -> &str; |
| 22 | +} |
| 23 | +# |
| 24 | +# fn main() {} |
| 25 | +``` |
| 26 | + |
| 27 | +However, to return values of such [interface][1], we should provide its implementers and the Rust type representing a _boxed value of this trait_. The last one can be represented in two flavors: enum and [trait object][2]. |
| 28 | + |
| 29 | + |
| 30 | +### Enum values (default) |
| 31 | + |
| 32 | +By default, Juniper generates an enum representing the values of the defined [GraphQL interface][1], and names it straightforwardly, `{Interface}Value`. |
| 33 | + |
| 34 | +```rust |
| 35 | +# extern crate juniper; |
| 36 | +use juniper::{graphql_interface, GraphQLObject}; |
| 37 | + |
| 38 | +#[graphql_interface(for = Human)] // enumerating all implementers is mandatory |
| 39 | +trait Character { |
| 40 | + fn id(&self) -> &str; |
| 41 | +} |
| 42 | + |
| 43 | +#[derive(GraphQLObject)] |
| 44 | +#[graphql(impl = CharacterValue)] // notice enum name, NOT trait name |
| 45 | +struct Human { |
| 46 | + id: String, |
| 47 | +} |
| 48 | +#[graphql_interface] // implementing requires macro attribute too, (°o°)! |
| 49 | +impl Character for Human { |
| 50 | + fn id(&self) -> &str { |
| 51 | + &self.id |
| 52 | + } |
| 53 | +} |
| 54 | +# |
| 55 | +# fn main() { |
| 56 | +let human = Human { id: "human-32".to_owned() }; |
| 57 | +// Values type for interface has `From` implementations for all its implementers, |
| 58 | +// so we don't need to bother with enum variant names. |
| 59 | +let character: CharacterValue = human.into(); |
| 60 | +assert_eq!(character.id(), "human-32"); |
| 61 | +# } |
| 62 | +``` |
| 63 | + |
| 64 | +Also, enum name can be specified explicitly, if desired. |
| 65 | + |
| 66 | +```rust |
| 67 | +# extern crate juniper; |
| 68 | +use juniper::{graphql_interface, GraphQLObject}; |
| 69 | + |
| 70 | +#[graphql_interface(enum = CharaterInterface, for = Human)] |
| 71 | +trait Character { |
| 72 | + fn id(&self) -> &str; |
| 73 | +} |
| 74 | + |
| 75 | +#[derive(GraphQLObject)] |
| 76 | +#[graphql(impl = CharaterInterface)] |
| 77 | +struct Human { |
| 78 | + id: String, |
| 79 | + home_planet: String, |
| 80 | +} |
| 81 | +#[graphql_interface] |
| 82 | +impl Character for Human { |
| 83 | + fn id(&self) -> &str { |
| 84 | + &self.id |
| 85 | + } |
| 86 | +} |
| 87 | +# |
| 88 | +# fn main() {} |
| 89 | +``` |
| 90 | + |
| 91 | + |
| 92 | +### Trait object values |
| 93 | + |
| 94 | +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. |
| 95 | + |
| 96 | +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. |
| 97 | + |
| 98 | +> __NOTICE__: |
| 99 | +> 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. |
| 100 | +
|
| 101 | +```rust |
| 102 | +# extern crate juniper; |
| 103 | +# extern crate tokio; |
| 104 | +use juniper::{graphql_interface, GraphQLObject}; |
| 105 | + |
| 106 | +// `dyn` argument accepts the name of type alias for the required trait object, |
| 107 | +// and macro generates this alias automatically |
| 108 | +#[graphql_interface(dyn = DynCharacter, for = Human)] |
| 109 | +trait Character { |
| 110 | + async fn id(&self) -> &str; // async fields are supported natively |
| 111 | +} |
| 112 | + |
| 113 | +#[derive(GraphQLObject)] |
| 114 | +#[graphql(impl = DynCharacter<__S>)] // macro adds `ScalarValue` type parameter to trait, |
| 115 | +struct Human { // so it may be specified explicitly when required |
| 116 | + id: String, |
| 117 | +} |
| 118 | +#[graphql_interface(dyn)] // implementing requires to know about dynamic dispatch too |
| 119 | +impl Character for Human { |
| 120 | + async fn id(&self) -> &str { |
| 121 | + &self.id |
| 122 | + } |
| 123 | +} |
| 124 | +# |
| 125 | +# #[tokio::main] |
| 126 | +# async fn main() { |
| 127 | +let human = Human { id: "human-32".to_owned() }; |
| 128 | +let character: Box<DynCharacter> = Box::new(human); |
| 129 | +assert_eq!(character.id().await, "human-32"); |
| 130 | +# } |
| 131 | +``` |
| 132 | + |
| 133 | + |
| 134 | +### Ignoring trait methods |
| 135 | + |
| 136 | +We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them. |
| 137 | + |
| 138 | +```rust |
| 139 | +# extern crate juniper; |
| 140 | +use juniper::{graphql_interface, GraphQLObject}; |
| 141 | + |
| 142 | +#[graphql_interface(for = Human)] |
| 143 | +trait Character { |
| 144 | + fn id(&self) -> &str; |
| 145 | + |
| 146 | + #[graphql_interface(ignore)] // or `#[graphql_interface(skip)]`, your choice |
| 147 | + fn ignored(&self) -> u32 { 0 } |
| 148 | +} |
| 149 | + |
| 150 | +#[derive(GraphQLObject)] |
| 151 | +#[graphql(impl = CharacterValue)] |
| 152 | +struct Human { |
| 153 | + id: String, |
| 154 | +} |
| 155 | +#[graphql_interface] // implementing requires macro attribute too, (°o°)! |
| 156 | +impl Character for Human { |
| 157 | + fn id(&self) -> &str { |
| 158 | + &self.id |
| 159 | + } |
| 160 | +} |
| 161 | +# |
| 162 | +# fn main() {} |
| 163 | +``` |
| 164 | + |
| 165 | + |
| 166 | +### Custom context |
| 167 | + |
| 168 | +If a context is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument. |
| 169 | + |
| 170 | +```rust |
| 171 | +# extern crate juniper; |
| 172 | +# use std::collections::HashMap; |
| 173 | +use juniper::{graphql_interface, GraphQLObject}; |
| 174 | + |
| 175 | +struct Database { |
| 176 | + humans: HashMap<String, Human>, |
| 177 | +} |
| 178 | +impl juniper::Context for Database {} |
| 179 | + |
| 180 | +#[graphql_interface(for = Human)] // look, ma, context type is inferred! \(^o^)/ |
| 181 | +trait Character { // while still can be specified via `Context = ...` attribute argument |
| 182 | + // If a field argument is named `context` or `ctx`, it's automatically assumed |
| 183 | + // as a context argument. |
| 184 | + fn id(&self, context: &Database) -> Option<&str>; |
| 185 | + |
| 186 | + // Otherwise, you may mark it explicitly as a context argument. |
| 187 | + fn name(&self, #[graphql_interface(context)] db: &Database) -> Option<&str>; |
| 188 | +} |
| 189 | + |
| 190 | +#[derive(GraphQLObject)] |
| 191 | +#[graphql(impl = CharacterValue, Context = Database)] |
| 192 | +struct Human { |
| 193 | + id: String, |
| 194 | + name: String, |
| 195 | +} |
| 196 | +#[graphql_interface] |
| 197 | +impl Character for Human { |
| 198 | + fn id(&self, db: &Database) -> Option<&str> { |
| 199 | + if db.humans.contains_key(&self.id) { |
| 200 | + Some(&self.id) |
| 201 | + } else { |
| 202 | + None |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + fn name(&self, db: &Database) -> Option<&str> { |
| 207 | + if db.humans.contains_key(&self.id) { |
| 208 | + Some(&self.name) |
| 209 | + } else { |
| 210 | + None |
| 211 | + } |
| 212 | + } |
| 213 | +} |
| 214 | +# |
| 215 | +# fn main() {} |
| 216 | +``` |
| 217 | + |
| 218 | + |
| 219 | +### Using executor and explicit generic scalar |
| 220 | + |
| 221 | +If an executor is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument. |
| 222 | + |
| 223 | +This requires to explicitly parametrize over [`ScalarValue`][3], as [`Executor`][4] does so. |
| 224 | + |
| 225 | +```rust |
| 226 | +# extern crate juniper; |
| 227 | +use juniper::{graphql_interface, Executor, GraphQLObject, LookAheadMethods as _, ScalarValue}; |
| 228 | + |
| 229 | +#[graphql_interface(for = Human, Scalar = S)] // notice specifying scalar as existing type parameter |
| 230 | +trait Character<S: ScalarValue> { |
| 231 | + // If a field argument is named `executor`, it's automatically assumed |
| 232 | + // as an executor argument. |
| 233 | + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str |
| 234 | + where |
| 235 | + S: Send + Sync; // required by `#[async_trait]` transformation ¯\_(ツ)_/¯ |
| 236 | + |
| 237 | + |
| 238 | + // Otherwise, you may mark it explicitly as an executor argument. |
| 239 | + async fn name<'b>( |
| 240 | + &'b self, |
| 241 | + #[graphql_interface(executor)] another: &Executor<'_, '_, (), S>, |
| 242 | + ) -> &'b str |
| 243 | + where |
| 244 | + S: Send + Sync; |
| 245 | +} |
| 246 | + |
| 247 | +#[derive(GraphQLObject)] |
| 248 | +#[graphql(impl = CharacterValue<__S>)] |
| 249 | +struct Human { |
| 250 | + id: String, |
| 251 | + name: String, |
| 252 | +} |
| 253 | +#[graphql_interface(Scalar = S)] |
| 254 | +impl<S: ScalarValue> Character<S> for Human { |
| 255 | + async fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str |
| 256 | + where |
| 257 | + S: Send + Sync, |
| 258 | + { |
| 259 | + executor.look_ahead().field_name() |
| 260 | + } |
| 261 | + |
| 262 | + async fn name<'b>(&'b self, _: &Executor<'_, '_, (), S>) -> &'b str |
| 263 | + where |
| 264 | + S: Send + Sync, |
| 265 | + { |
| 266 | + &self.name |
| 267 | + } |
| 268 | +} |
| 269 | +# |
| 270 | +# fn main() {} |
| 271 | +``` |
| 272 | + |
| 273 | + |
| 274 | + |
| 275 | + |
| 276 | + |
| 277 | + |
14 | 278 | Traits are maybe the most obvious concept you want to use when building
|
15 | 279 | interfaces. But because GraphQL supports downcasting while Rust doesn't, you'll
|
16 | 280 | have to manually specify how to convert a trait into a concrete type. This can
|
@@ -63,7 +327,7 @@ juniper::graphql_interface!(<'a> &'a dyn Character: () as "Character" where Scal
|
63 | 327 | # fn main() {}
|
64 | 328 | ```
|
65 | 329 |
|
66 |
| -The `instance_resolvers` declaration lists all the implementors of the given |
| 330 | +The `instance_resolvers` declaration lists all the implementers of the given |
67 | 331 | interface and how to resolve them.
|
68 | 332 |
|
69 | 333 | As you can see, you lose a bit of the point with using traits: you need to list
|
@@ -213,3 +477,12 @@ juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
|
213 | 477 |
|
214 | 478 | # fn main() {}
|
215 | 479 | ```
|
| 480 | + |
| 481 | + |
| 482 | + |
| 483 | + |
| 484 | + |
| 485 | +[1]: https://spec.graphql.org/June2018/#sec-Interfaces |
| 486 | +[2]: https://doc.rust-lang.org/reference/types/trait-object.html |
| 487 | +[3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html |
| 488 | +[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html |
0 commit comments