diff --git a/Cargo.toml b/Cargo.toml index 4234e409..05d49c77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,10 @@ uuid = { version = "0.8", features = ["serde", "v4"] } async-session = "3.0.0" tokio-stream = "0.1.7" +# for async-graphql example +async-graphql = "2.9.9" +slab = "0.4.3" + [dev-dependencies.tower] version = "0.4" features = [ @@ -96,3 +100,7 @@ required-features = ["headers"] [[example]] name = "websocket" required-features = ["ws", "headers"] + +[[example]] +name = "async-graphql" +required-features = ["headers"] diff --git a/deny.toml b/deny.toml index adc24c1a..575e337e 100644 --- a/deny.toml +++ b/deny.toml @@ -44,6 +44,12 @@ skip = [ { name = "crypto-mac" }, # async-session uses old version (dev dep) { name = "cfg-if", version = "=0.1.10" }, + # async-graphql has a few outdated deps (dev dep) + { name = "sha-1", version = "=0.8.2" }, + { name = "opaque-debug", version = "=0.2.3" }, + { name = "generic-array", version = "=0.12.4" }, + { name = "digest", version = "=0.8.1" }, + { name = "block-buffer", version = "=0.7.3" }, ] [sources] diff --git a/examples/README.md b/examples/README.md index 049df8ea..42b15a81 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,3 +17,4 @@ - [`tls_rustls`](../examples/tls_rustls.rs) - TLS with [`tokio-rustls`](https://crates.io/crates/tokio-rustls). - [`chat`](../examples/chat.rs) - Chat application example. - [`404`](../examples/404.rs) - Custom 404 page. +- [`async-graphql`](../examples/async-graphql) - GraphQL example using [`async-graphql`](https://crates.io/crates/async-graphql). diff --git a/examples/async-graphql/main.rs b/examples/async-graphql/main.rs new file mode 100644 index 00000000..13bf0d95 --- /dev/null +++ b/examples/async-graphql/main.rs @@ -0,0 +1,35 @@ +mod starwars; + +use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql::{EmptyMutation, EmptySubscription, Request, Response, Schema}; +use axum::response::IntoResponse; +use axum::{prelude::*, AddExtensionLayer}; +use starwars::{QueryRoot, StarWars, StarWarsSchema}; + +async fn graphql_handler( + schema: extract::Extension, + req: extract::Json, +) -> response::Json { + schema.execute(req.0).await.into() +} + +async fn graphql_playground() -> impl IntoResponse { + response::Html(playground_source(GraphQLPlaygroundConfig::new("/"))) +} + +#[tokio::main] +async fn main() { + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(StarWars::new()) + .finish(); + + let app = route("/", get(graphql_playground).post(graphql_handler)) + .layer(AddExtensionLayer::new(schema)); + + println!("Playground: http://localhost:3000"); + + hyper::Server::bind(&"0.0.0.0:3000".parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/examples/async-graphql/starwars/mod.rs b/examples/async-graphql/starwars/mod.rs new file mode 100644 index 00000000..53a54fc1 --- /dev/null +++ b/examples/async-graphql/starwars/mod.rs @@ -0,0 +1,139 @@ +mod model; + +use async_graphql::{EmptyMutation, EmptySubscription, Schema}; +use model::Episode; +use slab::Slab; +use std::collections::HashMap; + +pub use model::QueryRoot; +pub type StarWarsSchema = Schema; + +pub struct StarWarsChar { + id: &'static str, + name: &'static str, + friends: Vec, + appears_in: Vec, + home_planet: Option<&'static str>, + primary_function: Option<&'static str>, +} + +pub struct StarWars { + luke: usize, + artoo: usize, + chars: Slab, + human_data: HashMap<&'static str, usize>, + droid_data: HashMap<&'static str, usize>, +} + +impl StarWars { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let mut chars = Slab::new(); + + let luke = chars.insert(StarWarsChar { + id: "1000", + name: "Luke Skywalker", + friends: vec![], + appears_in: vec![], + home_planet: Some("Tatooine"), + primary_function: None, + }); + + let vader = chars.insert(StarWarsChar { + id: "1001", + name: "Luke Skywalker", + friends: vec![], + appears_in: vec![], + home_planet: Some("Tatooine"), + primary_function: None, + }); + + let han = chars.insert(StarWarsChar { + id: "1002", + name: "Han Solo", + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: None, + }); + + let leia = chars.insert(StarWarsChar { + id: "1003", + name: "Leia Organa", + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: Some("Alderaa"), + primary_function: None, + }); + + let tarkin = chars.insert(StarWarsChar { + id: "1004", + name: "Wilhuff Tarkin", + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: None, + }); + + let threepio = chars.insert(StarWarsChar { + id: "2000", + name: "C-3PO", + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: Some("Protocol"), + }); + + let artoo = chars.insert(StarWarsChar { + id: "2001", + name: "R2-D2", + friends: vec![], + appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi], + home_planet: None, + primary_function: Some("Astromech"), + }); + + chars[luke].friends = vec![han, leia, threepio, artoo]; + chars[vader].friends = vec![tarkin]; + chars[han].friends = vec![luke, leia, artoo]; + chars[leia].friends = vec![luke, han, threepio, artoo]; + chars[tarkin].friends = vec![vader]; + chars[threepio].friends = vec![luke, han, leia, artoo]; + chars[artoo].friends = vec![luke, han, leia]; + + let mut human_data = HashMap::new(); + human_data.insert("1000", luke); + human_data.insert("1001", vader); + human_data.insert("1002", han); + human_data.insert("1003", leia); + human_data.insert("1004", tarkin); + + let mut droid_data = HashMap::new(); + droid_data.insert("2000", threepio); + droid_data.insert("2001", artoo); + + Self { + luke, + artoo, + chars, + human_data, + droid_data, + } + } + + pub fn human(&self, id: &str) -> Option { + self.human_data.get(id).cloned() + } + + pub fn droid(&self, id: &str) -> Option { + self.droid_data.get(id).cloned() + } + + pub fn humans(&self) -> Vec { + self.human_data.values().cloned().collect() + } + + pub fn droids(&self) -> Vec { + self.droid_data.values().cloned().collect() + } +} diff --git a/examples/async-graphql/starwars/model.rs b/examples/async-graphql/starwars/model.rs new file mode 100644 index 00000000..9acb2de8 --- /dev/null +++ b/examples/async-graphql/starwars/model.rs @@ -0,0 +1,225 @@ +use super::StarWars; +use async_graphql::connection::{query, Connection, Edge, EmptyFields}; +use async_graphql::{Context, Enum, FieldResult, Interface, Object}; + +/// One of the films in the Star Wars Trilogy +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +pub enum Episode { + /// Released in 1977. + NewHope, + + /// Released in 1980. + Empire, + + /// Released in 1983. + Jedi, +} + +pub struct Human(usize); + +/// A humanoid creature in the Star Wars universe. +#[Object] +impl Human { + /// The id of the human. + async fn id(&self, ctx: &Context<'_>) -> &str { + ctx.data_unchecked::().chars[self.0].id + } + + /// The name of the human. + async fn name(&self, ctx: &Context<'_>) -> &str { + ctx.data_unchecked::().chars[self.0].name + } + + /// The friends of the human, or an empty list if they have none. + async fn friends(&self, ctx: &Context<'_>) -> Vec { + ctx.data_unchecked::().chars[self.0] + .friends + .iter() + .map(|id| Human(*id).into()) + .collect() + } + + /// Which movies they appear in. + async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] { + &ctx.data_unchecked::().chars[self.0].appears_in + } + + /// The home planet of the human, or null if unknown. + async fn home_planet<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> { + &ctx.data_unchecked::().chars[self.0].home_planet + } +} + +pub struct Droid(usize); + +/// A mechanical creature in the Star Wars universe. +#[Object] +impl Droid { + /// The id of the droid. + async fn id(&self, ctx: &Context<'_>) -> &str { + ctx.data_unchecked::().chars[self.0].id + } + + /// The name of the droid. + async fn name(&self, ctx: &Context<'_>) -> &str { + ctx.data_unchecked::().chars[self.0].name + } + + /// The friends of the droid, or an empty list if they have none. + async fn friends(&self, ctx: &Context<'_>) -> Vec { + ctx.data_unchecked::().chars[self.0] + .friends + .iter() + .map(|id| Droid(*id).into()) + .collect() + } + + /// Which movies they appear in. + async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] { + &ctx.data_unchecked::().chars[self.0].appears_in + } + + /// The primary function of the droid. + async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> { + &ctx.data_unchecked::().chars[self.0].primary_function + } +} + +pub struct QueryRoot; + +#[Object] +impl QueryRoot { + async fn hero( + &self, + ctx: &Context<'_>, + #[graphql( + desc = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode." + )] + episode: Episode, + ) -> Character { + if episode == Episode::Empire { + Human(ctx.data_unchecked::().luke).into() + } else { + Droid(ctx.data_unchecked::().artoo).into() + } + } + + async fn human( + &self, + ctx: &Context<'_>, + #[graphql(desc = "id of the human")] id: String, + ) -> Option { + ctx.data_unchecked::().human(&id).map(Human) + } + + async fn humans( + &self, + ctx: &Context<'_>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> FieldResult> { + let humans = ctx + .data_unchecked::() + .humans() + .iter() + .copied() + .collect::>(); + query_characters(after, before, first, last, &humans) + .await + .map(|conn| conn.map_node(Human)) + } + + async fn droid( + &self, + ctx: &Context<'_>, + #[graphql(desc = "id of the droid")] id: String, + ) -> Option { + ctx.data_unchecked::().droid(&id).map(Droid) + } + + async fn droids( + &self, + ctx: &Context<'_>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> FieldResult> { + let droids = ctx + .data_unchecked::() + .droids() + .iter() + .copied() + .collect::>(); + query_characters(after, before, first, last, &droids) + .await + .map(|conn| conn.map_node(Droid)) + } +} + +#[derive(Interface)] +#[graphql( + field(name = "id", type = "&str"), + field(name = "name", type = "&str"), + field(name = "friends", type = "Vec"), + field(name = "appears_in", type = "&'ctx [Episode]") +)] +pub enum Character { + Human(Human), + Droid(Droid), +} + +async fn query_characters( + after: Option, + before: Option, + first: Option, + last: Option, + characters: &[usize], +) -> FieldResult> { + query( + after, + before, + first, + last, + |after, before, first, last| async move { + let mut start = 0usize; + let mut end = characters.len(); + + if let Some(after) = after { + if after >= characters.len() { + return Ok(Connection::new(false, false)); + } + start = after + 1; + } + + if let Some(before) = before { + if before == 0 { + return Ok(Connection::new(false, false)); + } + end = before; + } + + let mut slice = &characters[start..end]; + + if let Some(first) = first { + slice = &slice[..first.min(slice.len())]; + end -= first.min(slice.len()); + } else if let Some(last) = last { + slice = &slice[slice.len() - last.min(slice.len())..]; + start = end - last.min(slice.len()); + } + + let mut connection = Connection::new(start > 0, end < characters.len()); + connection.append( + slice + .iter() + .enumerate() + .map(|(idx, item)| Edge::new(start + idx, *item)), + ); + Ok(connection) + }, + ) + .await +}