diff --git a/Cargo.toml b/Cargo.toml index de239bc8..383bdecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,4 +56,5 @@ members = [ "async-graphql-derive", "async-graphql-actix-web", "async-graphql-warp", + "async-graphql-tide", ] \ No newline at end of file diff --git a/async-graphql-tide/Cargo.toml b/async-graphql-tide/Cargo.toml new file mode 100644 index 00000000..177cde37 --- /dev/null +++ b/async-graphql-tide/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "async-graphql-tide" +version = "1.0.0" +authors = ["sunli "] +edition = "2018" +description = "async-graphql for tide" +publish = true +license = "MIT/Apache-2.0" +documentation = "https://docs.rs/async-graphql/" +homepage = "https://github.com/sunli829/async-graphql" +repository = "https://github.com/sunli829/async-graphql" +keywords = ["futures", "async", "graphql"] +categories = ["network-programming", "asynchronous"] + +[dependencies] +async-graphql = { path = "..", version = "1.9.18" } +tide = "0.8" +http-types = "1.2.0" + +[dev-dependencies] +async-std = "1.5.0" +surf = "2.0.0-alpha.1" +serde_json = "1.0.51" diff --git a/async-graphql-tide/src/lib.rs b/async-graphql-tide/src/lib.rs new file mode 100644 index 00000000..15675bfb --- /dev/null +++ b/async-graphql-tide/src/lib.rs @@ -0,0 +1,94 @@ +//! Async-graphql integration with Tide + +#![warn(missing_docs)] +#![allow(clippy::type_complexity)] +#![allow(clippy::needless_doctest_main)] + +use async_graphql::http::GQLResponse; +use async_graphql::{ + IntoQueryBuilder, IntoQueryBuilderOpts, ObjectType, QueryBuilder, Schema, SubscriptionType, +}; +use tide::{Request, Response, StatusCode}; + +/// GraphQL request handler +/// +/// It outputs a tuple containing the `Schema` and `QuertBuilder`. +/// +/// # Examples +/// *[Full Example]()* +/// +/// ```no_run +/// use async_graphql::*; +/// use async_std::task; +/// use tide::Request; +/// +/// struct QueryRoot; +/// #[Object] +/// impl QueryRoot { +/// #[field(desc = "Returns the sum of a and b")] +/// async fn add(&self, a: i32, b: i32) -> i32 { +/// a + b +/// } +/// } +/// +/// fn main() -> std::result::Result<(), Box> { +/// task::block_on(async { +/// let mut app = tide::new(); +/// app.at("/").post(|req: Request<()>| async move { +/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); +/// async_graphql_tide::graphql(req, schema, |query_builder| query_builder).await +/// }); +/// app.listen("0.0.0.0:8000").await?; +/// +/// Ok(()) +/// }) +/// } +/// ``` +pub async fn graphql( + req: Request, + schema: Schema, + query_builder_configuration: F, +) -> tide::Result +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, + TideState: Send + Sync + 'static, + F: Fn(QueryBuilder) -> QueryBuilder, +{ + graphql_opts(req, schema, query_builder_configuration, Default::default()).await +} + +/// Similar to graphql, but you can set the options `IntoQueryBuilderOpts`. +pub async fn graphql_opts( + req: Request, + schema: Schema, + query_builder_configuration: F, + opts: IntoQueryBuilderOpts, +) -> tide::Result +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, + TideState: Send + Sync + 'static, + F: Fn(QueryBuilder) -> QueryBuilder, +{ + let content_type = req + .header(&http_types::headers::CONTENT_TYPE) + .and_then(|values| values.first().map(|value| value.to_string())); + + let mut query_builder = (content_type, req) + .into_query_builder_opts(&opts) + .await + .map_err(|e| tide::Error::new(StatusCode::BadRequest, e))?; + + query_builder = query_builder_configuration(query_builder); + + let query_response = query_builder.execute(&schema).await; + + let gql_response = GQLResponse(query_response); + + let resp = Response::new(StatusCode::Ok).body_json(&gql_response)?; + + Ok(resp) +} diff --git a/async-graphql-tide/tests/graphql.rs b/async-graphql-tide/tests/graphql.rs new file mode 100644 index 00000000..cd4566c6 --- /dev/null +++ b/async-graphql-tide/tests/graphql.rs @@ -0,0 +1,119 @@ +mod test_utils; +use async_std::prelude::*; +use async_std::task; +use serde_json::json; +use std::time::Duration; +use tide::Request; + +use async_graphql::*; + +#[test] +fn quickstart() -> tide::Result<()> { + task::block_on(async { + let port = test_utils::find_port().await; + let server = task::spawn(async move { + struct QueryRoot; + #[Object] + impl QueryRoot { + #[field(desc = "Returns the sum of a and b")] + async fn add(&self, a: i32, b: i32) -> i32 { + a + b + } + } + + let mut app = tide::new(); + app.at("/").post(|req: Request<()>| async move { + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + async_graphql_tide::graphql(req, schema, |query_builder| query_builder).await + }); + app.listen(&port).await?; + + Ok(()) + }); + + let client = task::spawn(async move { + task::sleep(Duration::from_millis(100)).await; + let string = surf::post(format!("http://{}", port)) + .body_bytes(r#"{"query":"{ add(a: 10, b: 20) }"}"#) + .set_header("Content-Type".parse().unwrap(), "application/json") + .recv_string() + .await?; + assert_eq!(string, json!({"data": {"add": 30}}).to_string()); + Ok(()) + }); + + server.race(client).await + }) +} + +#[test] +fn hello() -> tide::Result<()> { + task::block_on(async { + let port = test_utils::find_port().await; + let server = task::spawn(async move { + struct Hello(String); + struct QueryRoot; + #[Object] + impl QueryRoot { + #[field(desc = "Returns hello")] + async fn hello<'a>(&self, ctx: &'a Context<'_>) -> String { + let name = ctx.data_opt::().map(|hello| hello.0.as_str()); + format!( + "Hello, {}!", + if let Some(name) = name { name } else { "world" } + ) + } + } + + struct ServerState { + schema: Schema, + } + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + + let server_state = ServerState { schema }; + let mut app = tide::with_state(server_state); + + app.at("/").post(|req: Request| async move { + let schema = req.state().schema.clone(); + let name = &req + .header(&"name".parse().unwrap()) + .and_then(|values| values.first().map(|value| value.to_string())); + + async_graphql_tide::graphql(req, schema, |mut query_builder| { + if let Some(name) = name { + query_builder = query_builder.data(Hello(name.to_string())) + } + query_builder + }) + .await + }); + app.listen(&port).await?; + + Ok(()) + }); + + let client = task::spawn(async move { + task::sleep(Duration::from_millis(100)).await; + let string = surf::post(format!("http://{}", port)) + .body_bytes(r#"{"query":"{ hello }"}"#) + .set_header("Content-Type".parse().unwrap(), "application/json") + .set_header("Name".parse().unwrap(), "Foo") + .recv_string() + .await?; + assert_eq!(string, json!({"data":{"hello":"Hello, Foo!"}}).to_string()); + + let string = surf::post(format!("http://{}", port)) + .body_bytes(r#"{"query":"{ hello }"}"#) + .set_header("Content-Type".parse().unwrap(), "application/json") + .recv_string() + .await?; + assert_eq!( + string, + json!({"data":{"hello":"Hello, world!"}}).to_string() + ); + Ok(()) + }); + + server.race(client).await + }) +} diff --git a/async-graphql-tide/tests/test_utils.rs b/async-graphql-tide/tests/test_utils.rs new file mode 100644 index 00000000..e9fceab0 --- /dev/null +++ b/async-graphql-tide/tests/test_utils.rs @@ -0,0 +1,7 @@ +pub async fn find_port() -> async_std::net::SocketAddr { + async_std::net::TcpListener::bind("localhost:0") + .await + .unwrap() + .local_addr() + .unwrap() +}