Improve Tide integration
This commit is contained in:
parent
a1d080dc15
commit
f764edc7d1
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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](<https://github.com/async-graphql/examples/blob/master/tide/starwars/src/main.rs>)*
|
||||
|
||||
|
@ -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},
|
||||
|
|
|
@ -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<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
#[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::<Result<()>>::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::<Result<()>>::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::<Result<()>>::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::<Hello>().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::<Hello>().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::<Result<()>>::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::<Result<()>>::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<String>,
|
||||
#[derive(Clone, SimpleObject)]
|
||||
pub struct FileInfo {
|
||||
filename: String,
|
||||
mime_type: Option<String>,
|
||||
}
|
||||
|
||||
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::<Result<()>>::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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user