diff --git a/Cargo.toml b/Cargo.toml index 1981d623..aa398824 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "async_db", "async_ex1", "async_ex2", + "async-graphql-demo", "async_pg", "awc_https", "basics", @@ -51,5 +52,5 @@ members = [ "websocket-autobahn", "websocket-chat", "websocket-chat-broker", - "websocket-tcp-chat", + "websocket-tcp-chat" ] diff --git a/async-graphql-demo/Cargo.toml b/async-graphql-demo/Cargo.toml new file mode 100644 index 00000000..48fd5995 --- /dev/null +++ b/async-graphql-demo/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "async-graphql-demo" +version = "0.1.0" +authors = ["sunli "] +edition = "2018" + +[dependencies] +actix-web = "3.0.0" +async-graphql = "2.0.0-alpha.7" +async-graphql-actix-web = "2.0.0-alpha.7" +slab = "0.4.2" diff --git a/async-graphql-demo/src/README.md b/async-graphql-demo/src/README.md new file mode 100644 index 00000000..067fbf8e --- /dev/null +++ b/async-graphql-demo/src/README.md @@ -0,0 +1,33 @@ +Getting started using [Async-graphql](https://github.com/async-graphql/async-graphql) with Actix web. + +## Run + +```bash +cargo run --bin async-graphql-demo +``` + +## Endpoints + + GET http://127.0.0.1:8000/ GraphQL Playground UI + POST http://127.0.0.1:8000/ For GraphQL query + +## Query Examples + +```graphql +{ + humans { + edges { + node { + id + name + friends { + id + name + } + appearsIn + homePlanet + } + } + } +} +``` \ No newline at end of file diff --git a/async-graphql-demo/src/main.rs b/async-graphql-demo/src/main.rs new file mode 100644 index 00000000..06f12dd1 --- /dev/null +++ b/async-graphql-demo/src/main.rs @@ -0,0 +1,38 @@ +mod starwars; + +use actix_web::{guard, web, App, HttpResponse, HttpServer, Result}; +use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema}; +use async_graphql_actix_web::{GQLRequest, GQLResponse}; +use starwars::{QueryRoot, StarWars, StarWarsSchema}; + +async fn index(schema: web::Data, req: GQLRequest) -> GQLResponse { + schema.execute(req.into_inner()).await.into() +} + +async fn index_playground() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(playground_source( + GraphQLPlaygroundConfig::new("/").subscription_endpoint("/"), + ))) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(StarWars::new()) + .finish(); + + println!("Playground: http://localhost:8000"); + + HttpServer::new(move || { + App::new() + .data(schema.clone()) + .service(web::resource("/").guard(guard::Post()).to(index)) + .service(web::resource("/").guard(guard::Get()).to(index_playground)) + }) + .bind("127.0.0.1:8000")? + .run() + .await +} diff --git a/async-graphql-demo/src/starwars/mod.rs b/async-graphql-demo/src/starwars/mod.rs new file mode 100644 index 00000000..53a54fc1 --- /dev/null +++ b/async-graphql-demo/src/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/async-graphql-demo/src/starwars/model.rs b/async-graphql-demo/src/starwars/model.rs new file mode 100644 index 00000000..f8001fa8 --- /dev/null +++ b/async-graphql-demo/src/starwars/model.rs @@ -0,0 +1,225 @@ +use super::StarWars; +use async_graphql::connection::{query, Connection, Edge, EmptyFields}; +use async_graphql::{Context, FieldResult, GQLEnum, GQLInterface, GQLObject}; + +/// One of the films in the Star Wars Trilogy +#[derive(GQLEnum, 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. +#[GQLObject] +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. +#[GQLObject] +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; + +#[GQLObject] +impl QueryRoot { + async fn hero( + &self, + ctx: &Context<'_>, + #[arg( + 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<'_>, + #[arg(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<'_>, + #[arg(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(GQLInterface)] +#[graphql( + field(name = "id", type = "&str", context), + field(name = "name", type = "&str", context), + field(name = "friends", type = "Vec", context), + field(name = "appears_in", type = "&'ctx [Episode]", context) +)] +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 +}