diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index 381548f4..b4e40d0b 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -74,7 +74,7 @@ impl FromRequest for BatchRequest { if req.method() == Method::GET { let res = serde_urlencoded::from_str(req.query_string()); - Box::pin(async move { Ok(Self(res?)) }) + Box::pin(async move { Ok(Self(async_graphql::BatchRequest::Single(res?))) }) } else { let content_type = req .headers() diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index 0accf9d1..1158b70e 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -14,13 +14,11 @@ categories = ["network-programming", "asynchronous"] [dependencies] async-graphql = { path = "../..", version = "=2.0.3" } + tide = { version = "0.13.0", default-features = false, features = ["h1-server"] } -async-trait = "0.1.36" -serde_json = "1.0.56" -futures = "0.3.5" -async-std = "1.6.2" -pin-project-lite = "0.1.9" [dev-dependencies] -smol = { version = "0.1.18", features = ["tokio02"] } -reqwest = "0.10.6" +# Surf lacks multipart support +reqwest = { version = "0.10.8", default-features = false, features = ["json"] } +tokio = { version = "0.2.22", default-features = false, features = ["rt-threaded", "macros"] } +serde_json = "1.0.59" diff --git a/integrations/tide/src/lib.rs b/integrations/tide/src/lib.rs index 2c4314b8..6ff08176 100644 --- a/integrations/tide/src/lib.rs +++ b/integrations/tide/src/lib.rs @@ -1,5 +1,8 @@ //! Async-graphql integration with Tide //! +//! Tide [does not support websockets](https://github.com/http-rs/tide/issues/67), so you can't use +//! subscriptions with it. +//! //! # Examples //! *[Full Example]()* @@ -10,7 +13,7 @@ use async_graphql::http::MultipartOptions; use async_graphql::{ObjectType, ParseRequestError, Schema, SubscriptionType}; -use async_trait::async_trait; +use tide::utils::async_trait; use tide::{ http::{ headers::{self, HeaderValue}, diff --git a/integrations/tide/tests/graphql.rs b/integrations/tide/tests/graphql.rs index 77ddd195..ed3cc569 100644 --- a/integrations/tide/tests/graphql.rs +++ b/integrations/tide/tests/graphql.rs @@ -1,258 +1,210 @@ mod test_utils; -use serde_json::json; -use smol::{Task, Timer}; + use std::io::Read; -use std::time::Duration; + +use reqwest::{header, StatusCode}; +use serde_json::json; use async_graphql::*; type Result = std::result::Result>; -#[test] -fn quickstart() -> Result<()> { - smol::run(async { - let listen_addr = test_utils::find_listen_addr().await; +#[tokio::test(max_threads = 1)] +async fn quickstart() -> Result<()> { + let listen_addr = test_utils::find_listen_addr(); - let server = Task::>::spawn(async move { - struct QueryRoot; - #[Object] - impl QueryRoot { - /// Returns the sum of a and b - async fn add(&self, a: i32, b: i32) -> i32 { - a + b - } + tokio::spawn(async move { + struct QueryRoot; + #[Object] + impl QueryRoot { + /// Returns the sum of a and b + async fn add(&self, a: i32, b: i32) -> i32 { + a + b } + } - let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); - let mut app = tide::new(); - let endpoint = async_graphql_tide::endpoint(schema); - app.at("/").post(endpoint.clone()).get(endpoint); - app.listen(listen_addr).await?; + let mut app = tide::new(); + let endpoint = async_graphql_tide::endpoint(schema); + app.at("/").post(endpoint.clone()).get(endpoint); + app.listen(listen_addr).await + }); - Ok(()) - }); + test_utils::wait_server_ready().await; - let client = Task::>::spawn(async move { - Timer::after(Duration::from_millis(300)).await; + let client = test_utils::client(); - let resp = reqwest::Client::builder() - .no_proxy() - .build() - .unwrap() - .post(format!("http://{}", listen_addr).as_str()) - .body(r#"{"query":"{ add(a: 10, b: 20) }"}"#) - .header(reqwest::header::CONTENT_TYPE, "application/json") - .send() - .await?; + let resp = client + .post(listen_addr) + .json(&json!({"query":"{ add(a: 10, b: 20) }"})) + .send() + .await?; - assert_eq!(resp.status(), reqwest::StatusCode::OK); - let string = resp.text().await?; - println!("via post {}", string); + assert_eq!(resp.status(), StatusCode::OK); + let string = resp.text().await?; + println!("via post {}", string); - assert_eq!(string, json!({"data": {"add": 30}}).to_string()); + assert_eq!(string, json!({"data": {"add": 30}}).to_string()); - // - let resp = reqwest::Client::builder() - .no_proxy() - .build() - .unwrap() - .get(format!("http://{}", listen_addr).as_str()) - .query(&[("query", "{ add(a: 10, b: 20) }")]) - .send() - .await?; + // + let resp = client + .get(listen_addr) + .query(&[("query", "{ add(a: 10, b: 20) }")]) + .send() + .await?; - assert_eq!(resp.status(), reqwest::StatusCode::OK); - let string = resp.text().await?; - println!("via get {}", string); + assert_eq!(resp.status(), StatusCode::OK); + let string = resp.text().await?; + println!("via get {}", string); - assert_eq!(string, json!({"data": {"add": 30}}).to_string()); + assert_eq!(string, json!({"data": {"add": 30}}).to_string()); - Ok(()) - }); - - client.await?; - server.cancel().await; - - Ok(()) - }) + Ok(()) } -#[test] -fn hello() -> Result<()> { - smol::run(async { - let listen_addr = test_utils::find_listen_addr().await; +#[tokio::test(max_threads = 1)] +async fn hello() -> Result<()> { + let listen_addr = test_utils::find_listen_addr(); - let server = Task::>::spawn(async move { - use tide::Request; - - struct Hello(String); - struct QueryRoot; - #[Object] - impl QueryRoot { - /// Returns hello - async fn hello<'a>(&self, ctx: &'a Context<'_>) -> String { - let name = ctx.data_opt::().map(|hello| hello.0.as_str()); - format!("Hello, {}!", name.unwrap_or("world")) - } + tokio::spawn(async move { + struct Hello(String); + struct QueryRoot; + #[Object] + impl QueryRoot { + /// Returns hello + async fn hello<'a>(&self, ctx: &'a Context<'_>) -> String { + let name = ctx.data_opt::().map(|hello| hello.0.as_str()); + format!("Hello, {}!", name.unwrap_or("world")) } + } - let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); + let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).finish(); - let mut app = tide::new(); + let mut app = tide::new(); - app.at("/").post(move |req: Request<()>| { - let schema = schema.clone(); - async move { - let name = req - .header("name") - .and_then(|values| values.get(0)) - .map(ToString::to_string); - let mut req = async_graphql_tide::receive_request(req).await?; - if let Some(name) = name { - req = req.data(Hello(name)); - } - async_graphql_tide::respond(schema.execute(req).await) + app.at("/").post(move |req: tide::Request<()>| { + let schema = schema.clone(); + async move { + let name = req + .header("name") + .and_then(|values| values.get(0)) + .map(ToString::to_string); + let mut req = async_graphql_tide::receive_request(req).await?; + if let Some(name) = name { + req = req.data(Hello(name)); } - }); - app.listen(listen_addr).await?; - - Ok(()) + async_graphql_tide::respond(schema.execute(req).await) + } }); + app.listen(listen_addr).await + }); - let client = Task::>::spawn(async move { - Timer::after(Duration::from_millis(300)).await; + test_utils::wait_server_ready().await; - let resp = reqwest::Client::builder() - .no_proxy() - .build() - .unwrap() - .post(format!("http://{}", listen_addr).as_str()) - .body(r#"{"query":"{ hello }"}"#) - .header(reqwest::header::CONTENT_TYPE, "application/json") - .header("Name", "Foo") - .send() - .await?; + let client = test_utils::client(); - assert_eq!(resp.status(), reqwest::StatusCode::OK); - let string = resp.text().await?; - println!("{}", string); + let resp = client + .post(listen_addr) + .json(&json!({"query":"{ hello }"})) + .header("Name", "Foo") + .send() + .await?; - assert_eq!(string, json!({"data":{"hello":"Hello, Foo!"}}).to_string()); + assert_eq!(resp.status(), StatusCode::OK); + let string = resp.text().await?; + println!("{}", string); - let resp = reqwest::Client::builder() - .no_proxy() - .build() - .unwrap() - .post(format!("http://{}", listen_addr).as_str()) - .body(r#"{"query":"{ hello }"}"#) - .header(reqwest::header::CONTENT_TYPE, "application/json") - .send() - .await?; + assert_eq!(string, json!({"data":{"hello":"Hello, Foo!"}}).to_string()); - assert_eq!(resp.status(), reqwest::StatusCode::OK); - let string = resp.text().await?; - println!("{}", string); + let resp = client + .post(listen_addr) + .json(&json!({"query":"{ hello }"})) + .header(header::CONTENT_TYPE, "application/json") + .send() + .await?; - assert_eq!( - string, - json!({"data":{"hello":"Hello, world!"}}).to_string() - ); + assert_eq!(resp.status(), StatusCode::OK); + let string = resp.text().await?; + println!("{}", string); - Ok(()) - }); + assert_eq!( + string, + json!({"data":{"hello":"Hello, world!"}}).to_string() + ); - client.await?; - server.cancel().await; - - Ok(()) - }) + Ok(()) } -#[test] -fn upload() -> Result<()> { - smol::run(async { - let listen_addr = test_utils::find_listen_addr().await; +#[tokio::test(max_threads = 1)] +async fn upload() -> Result<()> { + let listen_addr = test_utils::find_listen_addr(); - let server = Task::>::spawn(async move { - struct QueryRoot; - #[Object] - impl QueryRoot {} + tokio::spawn(async move { + struct QueryRoot; + #[Object] + impl QueryRoot {} - #[derive(Clone, SimpleObject)] - pub struct FileInfo { - filename: String, - mime_type: Option, + #[derive(Clone, SimpleObject)] + pub struct FileInfo { + filename: String, + mime_type: Option, + } + + struct MutationRoot; + #[Object] + impl MutationRoot { + async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo { + let upload_value = file.value(ctx).unwrap(); + println!("single_upload: filename={}", upload_value.filename); + println!( + "single_upload: content_type={:?}", + upload_value.content_type + ); + + let file_info = FileInfo { + filename: upload_value.filename.clone(), + mime_type: upload_value.content_type.clone(), + }; + + let mut content = String::new(); + upload_value + .into_read() + .read_to_string(&mut content) + .unwrap(); + assert_eq!(content, "test".to_owned()); + + file_info } + } - struct MutationRoot; - #[Object] - impl MutationRoot { - async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo { - let upload_value = file.value(ctx).unwrap(); - println!("single_upload: filename={}", upload_value.filename); - println!( - "single_upload: content_type={:?}", - upload_value.content_type - ); + let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription).finish(); - let file_info = FileInfo { - filename: upload_value.filename.clone(), - mime_type: upload_value.content_type.clone(), - }; + let mut app = tide::new(); + app.at("/").post(async_graphql_tide::endpoint(schema)); + app.listen(listen_addr).await + }); - let mut content = String::new(); - upload_value - .into_read() - .read_to_string(&mut content) - .unwrap(); - assert_eq!(content, "test".to_owned()); + test_utils::wait_server_ready().await; - file_info - } - } + let client = test_utils::client(); - let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription).finish(); + let form = reqwest::multipart::Form::new() + .text("operations", r#"{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { filename, mimeType } }", "variables": { "file": null } }"#) + .text("map", r#"{ "0": ["variables.file"] }"#) + .part("0", reqwest::multipart::Part::stream("test").file_name("test.txt").mime_str("text/plain")?); - let mut app = tide::new(); - app.at("/").post(async_graphql_tide::endpoint(schema)); - app.listen(listen_addr).await?; + let resp = client.post(listen_addr).multipart(form).send().await?; - Ok(()) - }); + assert_eq!(resp.status(), StatusCode::OK); + let string = resp.text().await?; + println!("{}", string); - let client = Task::>::spawn(async move { - Timer::after(Duration::from_millis(300)).await; + assert_eq!( + string, + json!({"data": {"singleUpload": {"filename": "test.txt", "mimeType": "text/plain"}}}) + .to_string() + ); - let form = reqwest::multipart::Form::new() - .text("operations", r#"{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { filename, mimeType } }", "variables": { "file": null } }"#) - .text("map", r#"{ "0": ["variables.file"] }"#) - .part("0", reqwest::multipart::Part::stream("test").file_name("test.txt").mime_str("text/plain")?); - - let resp = reqwest::Client::builder() - .no_proxy() - .build() - .unwrap() - .post(format!("http://{}", listen_addr).as_str()) - .multipart(form) - .send() - .await?; - - assert_eq!(resp.status(), reqwest::StatusCode::OK); - let string = resp.text().await?; - println!("{}", string); - - assert_eq!( - string, - json!({"data": {"singleUpload": {"filename": "test.txt", "mimeType": "text/plain"}}}).to_string() - ); - - Ok(()) - }); - - client.await?; - server.cancel().await; - - Ok(()) - }) + Ok(()) } diff --git a/integrations/tide/tests/test_utils.rs b/integrations/tide/tests/test_utils.rs index 6fa289a0..d9f655bc 100644 --- a/integrations/tide/tests/test_utils.rs +++ b/integrations/tide/tests/test_utils.rs @@ -1,6 +1,23 @@ -pub async fn find_listen_addr() -> std::net::SocketAddr { - std::net::TcpListener::bind("localhost:0") - .unwrap() - .local_addr() - .unwrap() +use reqwest::Client; +use std::time::Duration; + +pub fn find_listen_addr() -> &'static str { + Box::leak( + format!( + "http://{}", + std::net::TcpListener::bind("localhost:0") + .unwrap() + .local_addr() + .unwrap() + ) + .into_boxed_str(), + ) +} + +pub fn client() -> Client { + Client::builder().no_proxy().build().unwrap() +} + +pub async fn wait_server_ready() { + tokio::time::delay_for(Duration::from_millis(300)).await; }