* Support 'application/graphql' POST requests for 'juniper_warp' * Add integration tests for 'application/graphql' POST requests and revive HttpIntegration test suite for 'juniper_warp' * Support 'application/graphql' POST requests for 'juniper_hyper' and run its tests for both sync and async versions * Run integration tests for both sync and async versions of 'juniper_warp' and update its CHANGELOG * Support 'application/graphql' POST requests for 'juniper_iron' * Fix 'application/graphql' POST requests support for 'juniper_actix' * Support 'application/graphql' POST requests in 'juniper_rocket' and 'juniper_rocket_async' * Upd juniper's CHANGELOG
This commit is contained in:
parent
d13305f202
commit
aedb2d1eb4
14 changed files with 622 additions and 423 deletions
|
@ -67,6 +67,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
|
||||||
`#[graphql(arguments(argA(name = "test")))]`
|
`#[graphql(arguments(argA(name = "test")))]`
|
||||||
(see [#631](https://github.com/graphql-rust/juniper/pull/631))
|
(see [#631](https://github.com/graphql-rust/juniper/pull/631))
|
||||||
|
|
||||||
|
- Integration tests:
|
||||||
|
Rename `http::tests::HTTPIntegration` as `http::tests::HttpIntegration`
|
||||||
|
and add support for `application/graphql` POST request.
|
||||||
|
|
||||||
# [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)
|
# [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)
|
||||||
|
|
||||||
- Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)
|
- Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)
|
||||||
|
|
|
@ -367,15 +367,23 @@ pub mod tests {
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalized way to make requests to the http framework
|
/// Normalized way to make requests to the HTTP framework integration we are testing.
|
||||||
/// integration we are testing.
|
pub trait HttpIntegration {
|
||||||
pub trait HTTPIntegration {
|
/// Sends GET HTTP request to this integration with the provided `url` parameters string,
|
||||||
|
/// and returns response returned by this integration.
|
||||||
fn get(&self, url: &str) -> TestResponse;
|
fn get(&self, url: &str) -> TestResponse;
|
||||||
fn post(&self, url: &str, body: &str) -> TestResponse;
|
|
||||||
|
/// Sends POST HTTP request to this integration with the provided JSON-encoded `body`, and
|
||||||
|
/// returns response returned by this integration.
|
||||||
|
fn post_json(&self, url: &str, body: &str) -> TestResponse;
|
||||||
|
|
||||||
|
/// Sends POST HTTP request to this integration with the provided raw GraphQL query as
|
||||||
|
/// `body`, and returns response returned by this integration.
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> TestResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub fn run_http_test_suite<T: HTTPIntegration>(integration: &T) {
|
pub fn run_http_test_suite<T: HttpIntegration>(integration: &T) {
|
||||||
println!("Running HTTP Test suite for integration");
|
println!("Running HTTP Test suite for integration");
|
||||||
|
|
||||||
println!(" - test_simple_get");
|
println!(" - test_simple_get");
|
||||||
|
@ -404,6 +412,12 @@ pub mod tests {
|
||||||
|
|
||||||
println!(" - test_duplicate_keys");
|
println!(" - test_duplicate_keys");
|
||||||
test_duplicate_keys(integration);
|
test_duplicate_keys(integration);
|
||||||
|
|
||||||
|
println!(" - test_graphql_post");
|
||||||
|
test_graphql_post(integration);
|
||||||
|
|
||||||
|
println!(" - test_invalid_graphql_post");
|
||||||
|
test_invalid_graphql_post(integration);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwrap_json_response(response: &TestResponse) -> Json {
|
fn unwrap_json_response(response: &TestResponse) -> Json {
|
||||||
|
@ -416,7 +430,7 @@ pub mod tests {
|
||||||
.expect("Could not parse JSON object")
|
.expect("Could not parse JSON object")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_simple_get<T: HTTPIntegration>(integration: &T) {
|
fn test_simple_get<T: HttpIntegration>(integration: &T) {
|
||||||
// {hero{name}}
|
// {hero{name}}
|
||||||
let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");
|
let response = integration.get("/?query=%7Bhero%7Bname%7D%7D");
|
||||||
|
|
||||||
|
@ -430,7 +444,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_encoded_get<T: HTTPIntegration>(integration: &T) {
|
fn test_encoded_get<T: HttpIntegration>(integration: &T) {
|
||||||
// query { human(id: "1000") { id, name, appearsIn, homePlanet } }
|
// query { human(id: "1000") { id, name, appearsIn, homePlanet } }
|
||||||
let response = integration.get(
|
let response = integration.get(
|
||||||
"/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D");
|
"/?query=query%20%7B%20human(id%3A%20%221000%22)%20%7B%20id%2C%20name%2C%20appearsIn%2C%20homePlanet%20%7D%20%7D");
|
||||||
|
@ -460,7 +474,7 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_get_with_variables<T: HTTPIntegration>(integration: &T) {
|
fn test_get_with_variables<T: HttpIntegration>(integration: &T) {
|
||||||
// query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
|
// query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }
|
||||||
// with variables = { "id": "1000" }
|
// with variables = { "id": "1000" }
|
||||||
let response = integration.get(
|
let response = integration.get(
|
||||||
|
@ -491,8 +505,8 @@ pub mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_simple_post<T: HTTPIntegration>(integration: &T) {
|
fn test_simple_post<T: HttpIntegration>(integration: &T) {
|
||||||
let response = integration.post("/", r#"{"query": "{hero{name}}"}"#);
|
let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);
|
||||||
|
|
||||||
assert_eq!(response.status_code, 200);
|
assert_eq!(response.status_code, 200);
|
||||||
assert_eq!(response.content_type, "application/json");
|
assert_eq!(response.content_type, "application/json");
|
||||||
|
@ -500,12 +514,12 @@ pub mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unwrap_json_response(&response),
|
unwrap_json_response(&response),
|
||||||
serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
|
serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
|
||||||
.expect("Invalid JSON constant in test")
|
.expect("Invalid JSON constant in test"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_batched_post<T: HTTPIntegration>(integration: &T) {
|
fn test_batched_post<T: HttpIntegration>(integration: &T) {
|
||||||
let response = integration.post(
|
let response = integration.post_json(
|
||||||
"/",
|
"/",
|
||||||
r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
|
r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
|
||||||
);
|
);
|
||||||
|
@ -516,42 +530,57 @@ pub mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unwrap_json_response(&response),
|
unwrap_json_response(&response),
|
||||||
serde_json::from_str::<Json>(
|
serde_json::from_str::<Json>(
|
||||||
r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#
|
r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#,
|
||||||
)
|
)
|
||||||
.expect("Invalid JSON constant in test")
|
.expect("Invalid JSON constant in test"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_empty_batched_post<T: HTTPIntegration>(integration: &T) {
|
fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
|
||||||
let response = integration.post("/", "[]");
|
let response = integration.post_json("/", "[]");
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_invalid_json<T: HTTPIntegration>(integration: &T) {
|
fn test_invalid_json<T: HttpIntegration>(integration: &T) {
|
||||||
let response = integration.get("/?query=blah");
|
let response = integration.get("/?query=blah");
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
let response = integration.post("/", r#"blah"#);
|
let response = integration.post_json("/", r#"blah"#);
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_invalid_field<T: HTTPIntegration>(integration: &T) {
|
fn test_invalid_field<T: HttpIntegration>(integration: &T) {
|
||||||
// {hero{blah}}
|
// {hero{blah}}
|
||||||
let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
|
let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
let response = integration.post("/", r#"{"query": "{hero{blah}}"}"#);
|
let response = integration.post_json("/", r#"{"query": "{hero{blah}}"}"#);
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_duplicate_keys<T: HTTPIntegration>(integration: &T) {
|
fn test_duplicate_keys<T: HttpIntegration>(integration: &T) {
|
||||||
// {hero{name}}
|
// {hero{name}}
|
||||||
let response = integration.get("/?query=%7B%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%2C%20%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%7D");
|
let response = integration.get("/?query=%7B%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%2C%20%22query%22%3A%20%22%7Bhero%7Bname%7D%7D%22%7D");
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
let response = integration.post(
|
let response =
|
||||||
"/",
|
integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#);
|
||||||
r#"
|
|
||||||
{"query": "{hero{name}}", "query": "{hero{name}}"}
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
assert_eq!(response.status_code, 400);
|
assert_eq!(response.status_code, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_graphql_post<T: HttpIntegration>(integration: &T) {
|
||||||
|
let resp = integration.post_graphql("/", r#"{hero{name}}"#);
|
||||||
|
|
||||||
|
assert_eq!(resp.status_code, 200);
|
||||||
|
assert_eq!(resp.content_type, "application/json");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unwrap_json_response(&resp),
|
||||||
|
serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
|
||||||
|
.expect("Invalid JSON constant in test"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_invalid_graphql_post<T: HttpIntegration>(integration: &T) {
|
||||||
|
let resp = integration.post_graphql("/", r#"{hero{name}"#);
|
||||||
|
|
||||||
|
assert_eq!(resp.status_code, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,15 +127,13 @@ where
|
||||||
let req = GraphQLRequest::from(get_req.into_inner());
|
let req = GraphQLRequest::from(get_req.into_inner());
|
||||||
let gql_response = req.execute(schema, context).await;
|
let gql_response = req.execute(schema, context).await;
|
||||||
let body_response = serde_json::to_string(&gql_response)?;
|
let body_response = serde_json::to_string(&gql_response)?;
|
||||||
let response = match gql_response.is_ok() {
|
let mut response = match gql_response.is_ok() {
|
||||||
true => HttpResponse::Ok()
|
true => HttpResponse::Ok(),
|
||||||
.content_type("application/json")
|
false => HttpResponse::BadRequest(),
|
||||||
.body(body_response),
|
|
||||||
false => HttpResponse::BadRequest()
|
|
||||||
.content_type("application/json")
|
|
||||||
.body(body_response),
|
|
||||||
};
|
};
|
||||||
Ok(response)
|
Ok(response
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(body_response))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actix GraphQL Handler for POST requests
|
/// Actix GraphQL Handler for POST requests
|
||||||
|
@ -160,13 +158,15 @@ where
|
||||||
.get(CONTENT_TYPE)
|
.get(CONTENT_TYPE)
|
||||||
.and_then(|hv| hv.to_str().ok());
|
.and_then(|hv| hv.to_str().ok());
|
||||||
let req = match content_type_header {
|
let req = match content_type_header {
|
||||||
Some("application/json") | Some("application/graphql") => {
|
Some("application/json") => {
|
||||||
let body_string = String::from_request(&req, &mut payload.into_inner()).await;
|
let body = String::from_request(&req, &mut payload.into_inner()).await?;
|
||||||
let body_string = body_string?;
|
serde_json::from_str::<GraphQLBatchRequest<S>>(&body).map_err(ErrorBadRequest)
|
||||||
match serde_json::from_str::<GraphQLBatchRequest<S>>(&body_string) {
|
}
|
||||||
Ok(req) => Ok(req),
|
Some("application/graphql") => {
|
||||||
Err(err) => Err(ErrorBadRequest(err)),
|
let body = String::from_request(&req, &mut payload.into_inner()).await?;
|
||||||
}
|
Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
|
||||||
|
body, None, None,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
_ => Err(ErrorUnsupportedMediaType(
|
_ => Err(ErrorUnsupportedMediaType(
|
||||||
"GraphQL requests should have content type `application/json` or `application/graphql`",
|
"GraphQL requests should have content type `application/json` or `application/graphql`",
|
||||||
|
@ -223,7 +223,7 @@ mod tests {
|
||||||
use actix_web::{dev::ServiceResponse, http, http::header::CONTENT_TYPE, test, App};
|
use actix_web::{dev::ServiceResponse, http, http::header::CONTENT_TYPE, test, App};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use juniper::{
|
use juniper::{
|
||||||
http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
|
http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
|
||||||
tests::{model::Database, schema::Query},
|
tests::{model::Database, schema::Query},
|
||||||
EmptyMutation, EmptySubscription, RootNode,
|
EmptyMutation, EmptySubscription, RootNode,
|
||||||
};
|
};
|
||||||
|
@ -446,63 +446,54 @@ mod tests {
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestActixWebIntegration {}
|
pub struct TestActixWebIntegration;
|
||||||
|
|
||||||
impl HTTPIntegration for TestActixWebIntegration {
|
impl TestActixWebIntegration {
|
||||||
fn get(&self, url: &str) -> TestResponse {
|
fn make_request(&self, req: test::TestRequest) -> TestResponse {
|
||||||
let url = url.to_string();
|
actix_rt::System::new("request").block_on(async move {
|
||||||
actix_rt::System::new("get_request").block_on(async move {
|
let schema = RootNode::new(
|
||||||
let schema: Schema = RootNode::new(
|
|
||||||
Query,
|
|
||||||
EmptyMutation::<Database>::new(),
|
|
||||||
EmptySubscription::<Database>::new(),
|
|
||||||
);
|
|
||||||
let req = test::TestRequest::get()
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.uri(&url.clone())
|
|
||||||
.to_request();
|
|
||||||
|
|
||||||
let mut app =
|
|
||||||
test::init_service(App::new().data(schema).route("/", web::get().to(index)))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let resp = test::call_service(&mut app, req).await;
|
|
||||||
let test_response = make_test_response(resp).await;
|
|
||||||
test_response
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn post(&self, url: &str, body: &str) -> TestResponse {
|
|
||||||
let url = url.to_string();
|
|
||||||
let body = body.to_string();
|
|
||||||
actix_rt::System::new("post_request").block_on(async move {
|
|
||||||
let schema: Schema = RootNode::new(
|
|
||||||
Query,
|
Query,
|
||||||
EmptyMutation::<Database>::new(),
|
EmptyMutation::<Database>::new(),
|
||||||
EmptySubscription::<Database>::new(),
|
EmptySubscription::<Database>::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let req = test::TestRequest::post()
|
|
||||||
.header("content-type", "application/json")
|
|
||||||
.set_payload(body)
|
|
||||||
.uri(&url.clone())
|
|
||||||
.to_request();
|
|
||||||
|
|
||||||
let mut app =
|
let mut app =
|
||||||
test::init_service(App::new().data(schema).route("/", web::post().to(index)))
|
test::init_service(App::new().data(schema).route("/", web::to(index))).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
let resp = test::call_service(&mut app, req).await;
|
let resp = test::call_service(&mut app, req.to_request()).await;
|
||||||
let test_response = make_test_response(resp).await;
|
make_test_response(resp).await
|
||||||
test_response
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn make_test_response(mut response: ServiceResponse) -> TestResponse {
|
impl HttpIntegration for TestActixWebIntegration {
|
||||||
let body = take_response_body_string(&mut response).await;
|
fn get(&self, url: &str) -> TestResponse {
|
||||||
let status_code = response.status().as_u16();
|
self.make_request(test::TestRequest::get().uri(url))
|
||||||
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
|
}
|
||||||
|
|
||||||
|
fn post_json(&self, url: &str, body: &str) -> TestResponse {
|
||||||
|
self.make_request(
|
||||||
|
test::TestRequest::post()
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.set_payload(body.to_string())
|
||||||
|
.uri(url),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
|
||||||
|
self.make_request(
|
||||||
|
test::TestRequest::post()
|
||||||
|
.header("content-type", "application/graphql")
|
||||||
|
.set_payload(body.to_string())
|
||||||
|
.uri(url),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_test_response(mut resp: ServiceResponse) -> TestResponse {
|
||||||
|
let body = take_response_body_string(&mut resp).await;
|
||||||
|
let status_code = resp.status().as_u16();
|
||||||
|
let content_type = resp.headers().get(CONTENT_TYPE).unwrap();
|
||||||
TestResponse {
|
TestResponse {
|
||||||
status_code: status_code as i32,
|
status_code: status_code as i32,
|
||||||
body: Some(body),
|
body: Some(body),
|
||||||
|
@ -512,6 +503,6 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_actix_web_integration() {
|
fn test_actix_web_integration() {
|
||||||
run_http_test_suite(&TestActixWebIntegration {});
|
run_http_test_suite(&TestActixWebIntegration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
- `juniper_hyper::graphiql` now requires a second parameter for subscriptions
|
- `juniper_hyper::graphiql` now requires a second parameter for subscriptions.
|
||||||
|
- `juniper_hyper::graphql` now executes the schema asynchronously. For blocking synchronous execution consider `juniper_hyper::graphql_sync` for use.
|
||||||
|
- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header.
|
||||||
|
|
||||||
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
|
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_hyper-0.5.2)
|
||||||
|
|
||||||
|
|
|
@ -8,17 +8,17 @@ use hyper::{
|
||||||
Body, Method, Request, Response, StatusCode,
|
Body, Method, Request, Response, StatusCode,
|
||||||
};
|
};
|
||||||
use juniper::{
|
use juniper::{
|
||||||
http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest},
|
http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest, GraphQLRequest},
|
||||||
GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue,
|
GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue,
|
||||||
};
|
};
|
||||||
use serde_json::error::Error as SerdeError;
|
use serde_json::error::Error as SerdeError;
|
||||||
use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
|
use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
pub async fn graphql<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
pub async fn graphql_sync<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
||||||
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
||||||
context: Arc<CtxT>,
|
context: Arc<CtxT>,
|
||||||
request: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, hyper::Error>
|
) -> Result<Response<Body>, hyper::Error>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync + 'static,
|
S: ScalarValue + Send + Sync + 'static,
|
||||||
|
@ -30,31 +30,16 @@ where
|
||||||
MutationT::TypeInfo: Send + Sync,
|
MutationT::TypeInfo: Send + Sync,
|
||||||
SubscriptionT::TypeInfo: Send + Sync,
|
SubscriptionT::TypeInfo: Send + Sync,
|
||||||
{
|
{
|
||||||
match *request.method() {
|
Ok(match parse_req(req).await {
|
||||||
Method::GET => {
|
Ok(req) => execute_request_sync(root_node, context, req).await,
|
||||||
let gql_req = parse_get_req(request);
|
Err(resp) => resp,
|
||||||
|
})
|
||||||
match gql_req {
|
|
||||||
Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await),
|
|
||||||
Err(err) => Ok(render_error(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Method::POST => {
|
|
||||||
let gql_req = parse_post_req(request.into_body()).await;
|
|
||||||
|
|
||||||
match gql_req {
|
|
||||||
Ok(gql_req) => Ok(execute_request(root_node, context, gql_req).await),
|
|
||||||
Err(err) => Ok(render_error(err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn graphql_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
pub async fn graphql<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
||||||
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
||||||
context: Arc<CtxT>,
|
context: Arc<CtxT>,
|
||||||
request: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> Result<Response<Body>, hyper::Error>
|
) -> Result<Response<Body>, hyper::Error>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync + 'static,
|
S: ScalarValue + Send + Sync + 'static,
|
||||||
|
@ -66,25 +51,31 @@ where
|
||||||
MutationT::TypeInfo: Send + Sync,
|
MutationT::TypeInfo: Send + Sync,
|
||||||
SubscriptionT::TypeInfo: Send + Sync,
|
SubscriptionT::TypeInfo: Send + Sync,
|
||||||
{
|
{
|
||||||
match *request.method() {
|
Ok(match parse_req(req).await {
|
||||||
Method::GET => {
|
Ok(req) => execute_request(root_node, context, req).await,
|
||||||
let gql_req = parse_get_req(request);
|
Err(resp) => resp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
match gql_req {
|
async fn parse_req<S: ScalarValue>(
|
||||||
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
|
req: Request<Body>,
|
||||||
Err(err) => Ok(render_error(err)),
|
) -> Result<GraphQLBatchRequest<S>, Response<Body>> {
|
||||||
}
|
match *req.method() {
|
||||||
}
|
Method::GET => parse_get_req(req),
|
||||||
Method::POST => {
|
Method::POST => {
|
||||||
let gql_req = parse_post_req(request.into_body()).await;
|
let content_type = req
|
||||||
|
.headers()
|
||||||
match gql_req {
|
.get(header::CONTENT_TYPE)
|
||||||
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
|
.map(HeaderValue::to_str);
|
||||||
Err(err) => Ok(render_error(err)),
|
match content_type {
|
||||||
|
Some(Ok("application/json")) => parse_post_json_req(req.into_body()).await,
|
||||||
|
Some(Ok("application/graphql")) => parse_post_graphql_req(req.into_body()).await,
|
||||||
|
_ => return Err(new_response(StatusCode::BAD_REQUEST)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Ok(new_response(StatusCode::METHOD_NOT_ALLOWED)),
|
_ => return Err(new_response(StatusCode::METHOD_NOT_ALLOWED)),
|
||||||
}
|
}
|
||||||
|
.map_err(|e| render_error(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_get_req<S: ScalarValue>(
|
fn parse_get_req<S: ScalarValue>(
|
||||||
|
@ -100,7 +91,7 @@ fn parse_get_req<S: ScalarValue>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_post_req<S: ScalarValue>(
|
async fn parse_post_json_req<S: ScalarValue>(
|
||||||
body: Body,
|
body: Body,
|
||||||
) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
|
) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
|
||||||
let chunk = hyper::body::to_bytes(body)
|
let chunk = hyper::body::to_bytes(body)
|
||||||
|
@ -114,6 +105,21 @@ async fn parse_post_req<S: ScalarValue>(
|
||||||
.map_err(GraphQLRequestError::BodyJSONError)
|
.map_err(GraphQLRequestError::BodyJSONError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn parse_post_graphql_req<S: ScalarValue>(
|
||||||
|
body: Body,
|
||||||
|
) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
|
||||||
|
let chunk = hyper::body::to_bytes(body)
|
||||||
|
.await
|
||||||
|
.map_err(GraphQLRequestError::BodyHyper)?;
|
||||||
|
|
||||||
|
let query = String::from_utf8(chunk.iter().cloned().collect())
|
||||||
|
.map_err(GraphQLRequestError::BodyUtf8)?;
|
||||||
|
|
||||||
|
Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
|
||||||
|
query, None, None,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn graphiql(
|
pub async fn graphiql(
|
||||||
graphql_endpoint: &str,
|
graphql_endpoint: &str,
|
||||||
subscriptions_endpoint: Option<&str>,
|
subscriptions_endpoint: Option<&str>,
|
||||||
|
@ -146,7 +152,7 @@ fn render_error(err: GraphQLRequestError) -> Response<Body> {
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_request<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
async fn execute_request_sync<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
||||||
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
||||||
context: Arc<CtxT>,
|
context: Arc<CtxT>,
|
||||||
request: GraphQLBatchRequest<S>,
|
request: GraphQLBatchRequest<S>,
|
||||||
|
@ -177,7 +183,7 @@ where
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_request_async<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
async fn execute_request<CtxT, QueryT, MutationT, SubscriptionT, S>(
|
||||||
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
root_node: Arc<RootNode<'static, QueryT, MutationT, SubscriptionT, S>>,
|
||||||
context: Arc<CtxT>,
|
context: Arc<CtxT>,
|
||||||
request: GraphQLBatchRequest<S>,
|
request: GraphQLBatchRequest<S>,
|
||||||
|
@ -319,19 +325,34 @@ mod tests {
|
||||||
use reqwest::{self, Response as ReqwestResponse};
|
use reqwest::{self, Response as ReqwestResponse};
|
||||||
use std::{net::SocketAddr, sync::Arc, thread, time::Duration};
|
use std::{net::SocketAddr, sync::Arc, thread, time::Duration};
|
||||||
|
|
||||||
struct TestHyperIntegration;
|
struct TestHyperIntegration {
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
impl http_tests::HTTPIntegration for TestHyperIntegration {
|
impl http_tests::HttpIntegration for TestHyperIntegration {
|
||||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||||
let url = format!("http://127.0.0.1:3001/graphql{}", url);
|
let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
|
||||||
make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url)))
|
make_test_response(reqwest::get(&url).expect(&format!("failed GET {}", url)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
let url = format!("http://127.0.0.1:3001/graphql{}", url);
|
let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let res = client
|
let res = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(body.to_string())
|
||||||
|
.send()
|
||||||
|
.expect(&format!("failed POST {}", url));
|
||||||
|
make_test_response(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
|
let url = format!("http://127.0.0.1:{}/graphql{}", self.port, url);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(&url)
|
||||||
|
.header(reqwest::header::CONTENT_TYPE, "application/graphql")
|
||||||
.body(body.to_string())
|
.body(body.to_string())
|
||||||
.send()
|
.send()
|
||||||
.expect(&format!("failed POST {}", url));
|
.expect(&format!("failed POST {}", url));
|
||||||
|
@ -356,9 +377,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn run_hyper_integration(is_sync: bool) {
|
||||||
async fn test_hyper_integration() {
|
let port = if is_sync { 3002 } else { 3001 };
|
||||||
let addr: SocketAddr = ([127, 0, 0, 1], 3001).into();
|
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
|
||||||
|
|
||||||
let db = Arc::new(Database::new());
|
let db = Arc::new(Database::new());
|
||||||
let root_node = Arc::new(RootNode::new(
|
let root_node = Arc::new(RootNode::new(
|
||||||
|
@ -386,11 +407,15 @@ mod tests {
|
||||||
};
|
};
|
||||||
async move {
|
async move {
|
||||||
if matches {
|
if matches {
|
||||||
super::graphql(root_node, ctx, req).await
|
if is_sync {
|
||||||
|
super::graphql_sync(root_node, ctx, req).await
|
||||||
|
} else {
|
||||||
|
super::graphql(root_node, ctx, req).await
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut response = Response::new(Body::empty());
|
let mut resp = Response::new(Body::empty());
|
||||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
*resp.status_mut() = StatusCode::NOT_FOUND;
|
||||||
Ok(response)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -409,7 +434,7 @@ mod tests {
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
thread::sleep(Duration::from_millis(10)); // wait 10ms for server to bind
|
thread::sleep(Duration::from_millis(10)); // wait 10ms for server to bind
|
||||||
let integration = TestHyperIntegration;
|
let integration = TestHyperIntegration { port };
|
||||||
http_tests::run_http_test_suite(&integration);
|
http_tests::run_http_test_suite(&integration);
|
||||||
shutdown.abort();
|
shutdown.abort();
|
||||||
});
|
});
|
||||||
|
@ -418,4 +443,14 @@ mod tests {
|
||||||
eprintln!("server error: {}", e);
|
eprintln!("server error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_hyper_integration() {
|
||||||
|
run_hyper_integration(false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sync_hyper_integration() {
|
||||||
|
run_hyper_integration(true).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions
|
- `juniper_iron::GraphiQLHandler::new` now requires a second parameter for subscriptions.
|
||||||
|
- `400 Bad Request` is now returned if POST HTTP request contains no or invalid `Content-Type` header.
|
||||||
|
|
||||||
# [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
|
# [[0.6.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_iron-0.6.2)
|
||||||
|
|
||||||
|
|
|
@ -114,10 +114,17 @@ extern crate iron_test;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
extern crate url;
|
extern crate url;
|
||||||
|
|
||||||
use iron::{itry, method, middleware::Handler, mime::Mime, prelude::*, status};
|
use iron::{
|
||||||
|
headers::ContentType,
|
||||||
|
itry, method,
|
||||||
|
middleware::Handler,
|
||||||
|
mime::{Mime, TopLevel},
|
||||||
|
prelude::*,
|
||||||
|
status,
|
||||||
|
};
|
||||||
use urlencoded::{UrlDecodingError, UrlEncodedQuery};
|
use urlencoded::{UrlDecodingError, UrlEncodedQuery};
|
||||||
|
|
||||||
use std::{error::Error, fmt, io::Read};
|
use std::{error::Error, fmt, io::Read, ops::Deref as _};
|
||||||
|
|
||||||
use serde_json::error::Error as SerdeError;
|
use serde_json::error::Error as SerdeError;
|
||||||
|
|
||||||
|
@ -228,32 +235,41 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||||
let url_query_string = req
|
let url_query = req
|
||||||
.get_mut::<UrlEncodedQuery>()
|
.get_mut::<UrlEncodedQuery>()
|
||||||
.map_err(GraphQLIronError::Url)?;
|
.map_err(GraphQLIronError::Url)?;
|
||||||
|
|
||||||
let input_query = parse_url_param(url_query_string.remove("query"))?
|
let query = parse_url_param(url_query.remove("query"))?
|
||||||
.ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
|
.ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
|
||||||
let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
|
let operation_name = parse_url_param(url_query.remove("operationName"))?;
|
||||||
let variables = parse_variable_param(url_query_string.remove("variables"))?;
|
let variables = parse_variable_param(url_query.remove("variables"))?;
|
||||||
|
|
||||||
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||||
input_query,
|
query,
|
||||||
operation_name,
|
operation_name,
|
||||||
variables,
|
variables,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
fn handle_post_json(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||||
let mut request_payload = String::new();
|
let mut payload = String::new();
|
||||||
itry!(req.body.read_to_string(&mut request_payload));
|
itry!(req.body.read_to_string(&mut payload));
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
serde_json::from_str::<GraphQLBatchRequest<S>>(request_payload.as_str())
|
serde_json::from_str::<GraphQLBatchRequest<S>>(payload.as_str())
|
||||||
.map_err(GraphQLIronError::Serde)?,
|
.map_err(GraphQLIronError::Serde)?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_post_graphql(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||||
|
let mut payload = String::new();
|
||||||
|
itry!(req.body.read_to_string(&mut payload));
|
||||||
|
|
||||||
|
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||||
|
payload, None, None,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_sync(
|
fn execute_sync(
|
||||||
&self,
|
&self,
|
||||||
context: &CtxT,
|
context: &CtxT,
|
||||||
|
@ -313,7 +329,14 @@ where
|
||||||
|
|
||||||
let graphql_request = match req.method {
|
let graphql_request = match req.method {
|
||||||
method::Get => self.handle_get(&mut req)?,
|
method::Get => self.handle_get(&mut req)?,
|
||||||
method::Post => self.handle_post(&mut req)?,
|
method::Post => match req.headers.get::<ContentType>().map(ContentType::deref) {
|
||||||
|
Some(Mime(TopLevel::Application, sub_lvl, _)) => match sub_lvl.as_str() {
|
||||||
|
"json" => self.handle_post_json(&mut req)?,
|
||||||
|
"graphql" => self.handle_post_graphql(&mut req)?,
|
||||||
|
_ => return Ok(Response::with(status::BadRequest)),
|
||||||
|
},
|
||||||
|
_ => return Ok(Response::with(status::BadRequest)),
|
||||||
|
},
|
||||||
_ => return Ok(Response::with(status::MethodNotAllowed)),
|
_ => return Ok(Response::with(status::MethodNotAllowed)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -388,7 +411,11 @@ impl From<GraphQLIronError> for IronError {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use iron::{Handler, Headers, Url};
|
use iron::{
|
||||||
|
headers::ContentType,
|
||||||
|
mime::{Mime, SubLevel, TopLevel},
|
||||||
|
Handler, Headers, Url,
|
||||||
|
};
|
||||||
use iron_test::{request, response};
|
use iron_test::{request, response};
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||||
|
|
||||||
|
@ -411,7 +438,7 @@ mod tests {
|
||||||
.path()
|
.path()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| (*x).to_string())
|
.map(|x| (*x).to_string())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<_>>()
|
||||||
.join("/");
|
.join("/");
|
||||||
format!(
|
format!(
|
||||||
"http://localhost:3000{}?{}",
|
"http://localhost:3000{}?{}",
|
||||||
|
@ -422,21 +449,31 @@ mod tests {
|
||||||
|
|
||||||
struct TestIronIntegration;
|
struct TestIronIntegration;
|
||||||
|
|
||||||
impl http_tests::HTTPIntegration for TestIronIntegration {
|
impl http_tests::HttpIntegration for TestIronIntegration {
|
||||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||||
let result = request::get(&fixup_url(url), Headers::new(), &make_handler());
|
request::get(&fixup_url(url), Headers::new(), &make_handler())
|
||||||
match result {
|
.map(make_test_response)
|
||||||
Ok(response) => make_test_response(response),
|
.unwrap_or_else(make_test_error_response)
|
||||||
Err(e) => make_test_error_response(e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler());
|
let mut headers = Headers::new();
|
||||||
match result {
|
headers.set(ContentType::json());
|
||||||
Ok(response) => make_test_response(response),
|
request::post(&fixup_url(url), headers, body, &make_handler())
|
||||||
Err(e) => make_test_error_response(e),
|
.map(make_test_response)
|
||||||
}
|
.unwrap_or_else(make_test_error_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.set(ContentType(Mime(
|
||||||
|
TopLevel::Application,
|
||||||
|
SubLevel::Ext("graphql".into()),
|
||||||
|
vec![],
|
||||||
|
)));
|
||||||
|
request::post(&fixup_url(url), headers, body, &make_handler())
|
||||||
|
.map(make_test_response)
|
||||||
|
.unwrap_or_else(make_test_error_response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
- Rocket integration does not require default features.
|
- Rocket integration does not require default features.
|
||||||
|
- Support `application/graphql` POST requests.
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ use rocket::{
|
||||||
request::{FormItems, FromForm, FromFormValue},
|
request::{FormItems, FromForm, FromFormValue},
|
||||||
response::{content, Responder, Response},
|
response::{content, Responder, Response},
|
||||||
Data,
|
Data,
|
||||||
Outcome::{Failure, Forward, Success},
|
Outcome::{Forward, Success},
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -271,20 +271,26 @@ where
|
||||||
{
|
{
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
|
fn from_data(req: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
|
||||||
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
let content_type = req
|
||||||
return Forward(data);
|
.content_type()
|
||||||
}
|
.map(|ct| (ct.top().as_str(), ct.sub().as_str()));
|
||||||
|
let is_json = match content_type {
|
||||||
|
Some(("application", "json")) => true,
|
||||||
|
Some(("application", "graphql")) => false,
|
||||||
|
_ => return Forward(data),
|
||||||
|
};
|
||||||
|
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
if let Err(e) = data.open().read_to_string(&mut body) {
|
data.open()
|
||||||
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
.read_to_string(&mut body)
|
||||||
}
|
.map_err(|e| Err((Status::InternalServerError, format!("{:?}", e))))?;
|
||||||
|
|
||||||
match serde_json::from_str(&body) {
|
Success(GraphQLRequest(if is_json {
|
||||||
Ok(value) => Success(GraphQLRequest(value)),
|
serde_json::from_str(&body).map_err(|e| Err((Status::BadRequest, format!("{}", e))))?
|
||||||
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
|
} else {
|
||||||
}
|
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,16 +462,25 @@ mod tests {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl http_tests::HTTPIntegration for TestRocketIntegration {
|
impl http_tests::HttpIntegration for TestRocketIntegration {
|
||||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||||
let req = &self.client.get(url);
|
let req = &self.client.get(url);
|
||||||
make_test_response(req)
|
make_test_response(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
let req = &self.client.post(url).header(ContentType::JSON).body(body);
|
let req = &self.client.post(url).header(ContentType::JSON).body(body);
|
||||||
make_test_response(req)
|
make_test_response(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
|
let req = &self
|
||||||
|
.client
|
||||||
|
.post(url)
|
||||||
|
.header(ContentType::new("application", "graphql"))
|
||||||
|
.body(body);
|
||||||
|
make_test_response(req)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
- Compatibility with the latest `juniper`.
|
- Compatibility with the latest `juniper`.
|
||||||
- Rocket integration does not require default features.
|
- Rocket integration does not require default features.
|
||||||
|
- Support `application/graphql` POST requests.
|
||||||
|
|
||||||
# [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1)
|
# [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1)
|
||||||
|
|
||||||
|
|
|
@ -294,12 +294,17 @@ where
|
||||||
{
|
{
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
|
fn from_data(req: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
|
||||||
use tokio::io::AsyncReadExt as _;
|
use tokio::io::AsyncReadExt as _;
|
||||||
|
|
||||||
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
let content_type = req
|
||||||
return Box::pin(async move { Forward(data) });
|
.content_type()
|
||||||
}
|
.map(|ct| (ct.top().as_str(), ct.sub().as_str()));
|
||||||
|
let is_json = match content_type {
|
||||||
|
Some(("application", "json")) => true,
|
||||||
|
Some(("application", "graphql")) => false,
|
||||||
|
_ => return Box::pin(async move { Forward(data) }),
|
||||||
|
};
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
|
@ -308,10 +313,14 @@ where
|
||||||
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
match serde_json::from_str(&body) {
|
Success(GraphQLRequest(if is_json {
|
||||||
Ok(value) => Success(GraphQLRequest(value)),
|
match serde_json::from_str(&body) {
|
||||||
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
|
Ok(req) => req,
|
||||||
}
|
Err(e) => return Failure((Status::BadRequest, format!("{}", e))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -495,18 +504,28 @@ mod tests {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl http_tests::HTTPIntegration for TestRocketIntegration {
|
impl http_tests::HttpIntegration for TestRocketIntegration {
|
||||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||||
let req = self.client.get(url);
|
let req = self.client.get(url);
|
||||||
let req = futures::executor::block_on(req.dispatch());
|
let req = futures::executor::block_on(req.dispatch());
|
||||||
futures::executor::block_on(make_test_response(req))
|
futures::executor::block_on(make_test_response(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
let req = self.client.post(url).header(ContentType::JSON).body(body);
|
let req = self.client.post(url).header(ContentType::JSON).body(body);
|
||||||
let req = futures::executor::block_on(req.dispatch());
|
let req = futures::executor::block_on(req.dispatch());
|
||||||
futures::executor::block_on(make_test_response(req))
|
futures::executor::block_on(make_test_response(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.post(url)
|
||||||
|
.header(ContentType::new("application", "graphql"))
|
||||||
|
.body(body);
|
||||||
|
let req = futures::executor::block_on(req.dispatch());
|
||||||
|
futures::executor::block_on(make_test_response(req))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -6,10 +6,11 @@ to `juniper` to be reused in other http integrations, since this implementation
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
||||||
- Update `playground_filter` to support subscription endpoint URLs
|
- Update `playground_filter` to support subscription endpoint URLs.
|
||||||
- Update `warp` to 0.2
|
- Update `warp` to 0.2.
|
||||||
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
|
- Rename synchronous `execute` to `execute_sync`, add asynchronous `execute`
|
||||||
- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions
|
- `juniper_warp::graphiql_filter` now requires a second parameter for subscriptions.
|
||||||
|
- `make_graphql_filter` and `make_graphql_filter_sync` now ignore POST HTTP requests with no or invalid `Content-Type` header.
|
||||||
|
|
||||||
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
|
# [[0.5.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_warp-0.5.2)
|
||||||
|
|
||||||
|
|
|
@ -12,18 +12,19 @@ edition = "2018"
|
||||||
subscriptions = ["juniper_subscriptions"]
|
subscriptions = ["juniper_subscriptions"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
warp = "0.2"
|
bytes = "0.5"
|
||||||
|
failure = "0.1.7"
|
||||||
futures = "0.3.1"
|
futures = "0.3.1"
|
||||||
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
|
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
|
||||||
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true}
|
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true }
|
||||||
tokio = { version = "0.2", features = ["rt-core", "blocking"] }
|
|
||||||
serde = { version = "1.0.75", features = ["derive"] }
|
serde = { version = "1.0.75", features = ["derive"] }
|
||||||
serde_json = "1.0.24"
|
serde_json = "1.0.24"
|
||||||
failure = "0.1.7"
|
tokio = { version = "0.2", features = ["blocking", "rt-core"] }
|
||||||
|
warp = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
|
|
||||||
env_logger = "0.5.11"
|
env_logger = "0.5.11"
|
||||||
|
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
|
||||||
log = "0.4.3"
|
log = "0.4.3"
|
||||||
percent-encoding = "1.0"
|
percent-encoding = "1.0"
|
||||||
tokio = { version = "0.2", features = ["rt-core", "macros", "blocking"] }
|
tokio = { version = "0.2", features = ["blocking", "macros", "rt-core"] }
|
||||||
|
|
|
@ -40,12 +40,16 @@ Check the LICENSE file for details.
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
#![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")]
|
#![doc(html_root_url = "https://docs.rs/juniper_warp/0.2.0")]
|
||||||
|
|
||||||
use std::{pin::Pin, sync::Arc};
|
use std::{collections::HashMap, str, sync::Arc};
|
||||||
|
|
||||||
use futures::{Future, FutureExt as _, TryFutureExt};
|
use bytes::Bytes;
|
||||||
use juniper::{http::GraphQLBatchRequest, ScalarValue};
|
use futures::{FutureExt as _, TryFutureExt};
|
||||||
|
use juniper::{
|
||||||
|
http::{GraphQLBatchRequest, GraphQLRequest},
|
||||||
|
ScalarValue,
|
||||||
|
};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use warp::{filters::BoxedFilter, Filter};
|
use warp::{body, filters::BoxedFilter, header, http, query, Filter};
|
||||||
|
|
||||||
/// Make a filter for graphql queries/mutations.
|
/// Make a filter for graphql queries/mutations.
|
||||||
///
|
///
|
||||||
|
@ -110,7 +114,7 @@ use warp::{filters::BoxedFilter, Filter};
|
||||||
pub fn make_graphql_filter<Query, Mutation, Subscription, Context, S>(
|
pub fn make_graphql_filter<Query, Mutation, Subscription, Context, S>(
|
||||||
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||||
context_extractor: BoxedFilter<(Context,)>,
|
context_extractor: BoxedFilter<(Context,)>,
|
||||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
) -> BoxedFilter<(http::Response<Vec<u8>>,)>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync + 'static,
|
S: ScalarValue + Send + Sync + 'static,
|
||||||
Context: Send + Sync + 'static,
|
Context: Send + Sync + 'static,
|
||||||
|
@ -122,65 +126,90 @@ where
|
||||||
Subscription::TypeInfo: Send + Sync,
|
Subscription::TypeInfo: Send + Sync,
|
||||||
{
|
{
|
||||||
let schema = Arc::new(schema);
|
let schema = Arc::new(schema);
|
||||||
let post_schema = schema.clone();
|
let post_json_schema = schema.clone();
|
||||||
|
let post_graphql_schema = schema.clone();
|
||||||
|
|
||||||
let handle_post_request = move |context: Context, request: GraphQLBatchRequest<S>| {
|
let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
|
||||||
let schema = post_schema.clone();
|
let schema = post_json_schema.clone();
|
||||||
|
async move {
|
||||||
Box::pin(async move {
|
let resp = req.execute(&schema, &context).await;
|
||||||
let res = request.execute(&schema, &context).await;
|
|
||||||
|
|
||||||
Ok::<_, warp::Rejection>(build_response(
|
Ok::<_, warp::Rejection>(build_response(
|
||||||
serde_json::to_vec(&res)
|
serde_json::to_vec(&resp)
|
||||||
.map(|json| (json, res.is_ok()))
|
.map(|json| (json, resp.is_ok()))
|
||||||
.map_err(Into::into),
|
.map_err(Into::into),
|
||||||
))
|
))
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
|
let post_json_filter = warp::post()
|
||||||
let post_filter = warp::post()
|
.and(header::exact_ignore_case(
|
||||||
|
"content-type",
|
||||||
|
"application/json",
|
||||||
|
))
|
||||||
.and(context_extractor.clone())
|
.and(context_extractor.clone())
|
||||||
.and(warp::body::json())
|
.and(body::json())
|
||||||
.and_then(handle_post_request);
|
.and_then(handle_post_json_request);
|
||||||
|
|
||||||
let handle_get_request =
|
let handle_post_graphql_request = move |context: Context, body: Bytes| {
|
||||||
move |context: Context, mut request: std::collections::HashMap<String, String>| {
|
let schema = post_graphql_schema.clone();
|
||||||
let schema = schema.clone();
|
async move {
|
||||||
|
let query = str::from_utf8(body.as_ref()).map_err(|e| {
|
||||||
|
failure::format_err!("Request body query is not a valid UTF-8 string: {}", e)
|
||||||
|
})?;
|
||||||
|
let req = GraphQLRequest::new(query.into(), None, None);
|
||||||
|
|
||||||
async move {
|
let resp = req.execute(&schema, &context).await;
|
||||||
let variables = match request.remove("variables") {
|
|
||||||
None => None,
|
|
||||||
Some(vs) => serde_json::from_str(&vs)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let graphql_request = juniper::http::GraphQLRequest::new(
|
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||||
request.remove("query").ok_or_else(|| {
|
}
|
||||||
failure::format_err!("Missing GraphQL query string in query parameters")
|
.then(|res| async { Ok::<_, warp::Rejection>(build_response(res)) })
|
||||||
})?,
|
};
|
||||||
request.get("operation_name").map(|s| s.to_owned()),
|
let post_graphql_filter = warp::post()
|
||||||
variables,
|
.and(header::exact_ignore_case(
|
||||||
);
|
"content-type",
|
||||||
|
"application/graphql",
|
||||||
|
))
|
||||||
|
.and(context_extractor.clone())
|
||||||
|
.and(body::bytes())
|
||||||
|
.and_then(handle_post_graphql_request);
|
||||||
|
|
||||||
let response = graphql_request.execute(&schema, &context).await;
|
let handle_get_request = move |context: Context, mut qry: HashMap<String, String>| {
|
||||||
|
let schema = schema.clone();
|
||||||
|
async move {
|
||||||
|
let req = GraphQLRequest::new(
|
||||||
|
qry.remove("query").ok_or_else(|| {
|
||||||
|
failure::format_err!("Missing GraphQL query string in query parameters")
|
||||||
|
})?,
|
||||||
|
qry.remove("operation_name"),
|
||||||
|
qry.remove("variables")
|
||||||
|
.map(|vs| serde_json::from_str(&vs))
|
||||||
|
.transpose()?,
|
||||||
|
);
|
||||||
|
|
||||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
let resp = req.execute(&schema, &context).await;
|
||||||
}
|
|
||||||
.then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) })
|
|
||||||
};
|
|
||||||
|
|
||||||
|
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||||
|
}
|
||||||
|
.then(|res| async move { Ok::<_, warp::Rejection>(build_response(res)) })
|
||||||
|
};
|
||||||
let get_filter = warp::get()
|
let get_filter = warp::get()
|
||||||
.and(context_extractor)
|
.and(context_extractor)
|
||||||
.and(warp::filters::query::query())
|
.and(query::query())
|
||||||
.and_then(handle_get_request);
|
.and_then(handle_get_request);
|
||||||
|
|
||||||
get_filter.or(post_filter).unify().boxed()
|
get_filter
|
||||||
|
.or(post_json_filter)
|
||||||
|
.unify()
|
||||||
|
.or(post_graphql_filter)
|
||||||
|
.unify()
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a synchronous filter for graphql endpoint.
|
/// Make a synchronous filter for graphql endpoint.
|
||||||
pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
|
pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
|
||||||
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||||
context_extractor: BoxedFilter<(Context,)>,
|
context_extractor: BoxedFilter<(Context,)>,
|
||||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
) -> BoxedFilter<(http::Response<Vec<u8>>,)>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync + 'static,
|
S: ScalarValue + Send + Sync + 'static,
|
||||||
Context: Send + Sync + 'static,
|
Context: Send + Sync + 'static,
|
||||||
|
@ -189,105 +218,116 @@ where
|
||||||
Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let schema = Arc::new(schema);
|
let schema = Arc::new(schema);
|
||||||
let post_schema = schema.clone();
|
let post_json_schema = schema.clone();
|
||||||
|
let post_graphql_schema = schema.clone();
|
||||||
|
|
||||||
let handle_post_request =
|
let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
|
||||||
move |context: Context, request: GraphQLBatchRequest<S>| -> Response {
|
let schema = post_json_schema.clone();
|
||||||
let schema = post_schema.clone();
|
async move {
|
||||||
|
let res = task::spawn_blocking(move || {
|
||||||
|
let resp = req.execute_sync(&schema, &context);
|
||||||
|
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
Box::pin(
|
Ok(build_response(res))
|
||||||
async move {
|
}
|
||||||
let result = task::spawn_blocking(move || {
|
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
|
||||||
let response = request.execute_sync(&schema, &context);
|
|
||||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(build_response(result))
|
|
||||||
}
|
|
||||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let post_filter = warp::post()
|
|
||||||
.and(context_extractor.clone())
|
|
||||||
.and(warp::body::json())
|
|
||||||
.and_then(handle_post_request);
|
|
||||||
|
|
||||||
let handle_get_request = move |context: Context,
|
|
||||||
mut request: std::collections::HashMap<String, String>|
|
|
||||||
-> Response {
|
|
||||||
let schema = schema.clone();
|
|
||||||
|
|
||||||
Box::pin(
|
|
||||||
async move {
|
|
||||||
let result = task::spawn_blocking(move || {
|
|
||||||
let variables = match request.remove("variables") {
|
|
||||||
None => None,
|
|
||||||
Some(vs) => serde_json::from_str(&vs)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let graphql_request = juniper::http::GraphQLRequest::new(
|
|
||||||
request.remove("query").ok_or_else(|| {
|
|
||||||
failure::format_err!("Missing GraphQL query string in query parameters")
|
|
||||||
})?,
|
|
||||||
request.get("operation_name").map(|s| s.to_owned()),
|
|
||||||
variables,
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = graphql_request.execute_sync(&schema, &context);
|
|
||||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(build_response(result))
|
|
||||||
}
|
|
||||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
let post_json_filter = warp::post()
|
||||||
|
.and(header::exact_ignore_case(
|
||||||
|
"content-type",
|
||||||
|
"application/json",
|
||||||
|
))
|
||||||
|
.and(context_extractor.clone())
|
||||||
|
.and(body::json())
|
||||||
|
.and_then(handle_post_json_request);
|
||||||
|
|
||||||
|
let handle_post_graphql_request = move |context: Context, body: Bytes| {
|
||||||
|
let schema = post_graphql_schema.clone();
|
||||||
|
async move {
|
||||||
|
let res = task::spawn_blocking(move || {
|
||||||
|
let query = str::from_utf8(body.as_ref()).map_err(|e| {
|
||||||
|
failure::format_err!("Request body is not a valid UTF-8 string: {}", e)
|
||||||
|
})?;
|
||||||
|
let req = GraphQLRequest::new(query.into(), None, None);
|
||||||
|
|
||||||
|
let resp = req.execute_sync(&schema, &context);
|
||||||
|
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(build_response(res))
|
||||||
|
}
|
||||||
|
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
|
||||||
|
};
|
||||||
|
let post_graphql_filter = warp::post()
|
||||||
|
.and(header::exact_ignore_case(
|
||||||
|
"content-type",
|
||||||
|
"application/graphql",
|
||||||
|
))
|
||||||
|
.and(context_extractor.clone())
|
||||||
|
.and(body::bytes())
|
||||||
|
.and_then(handle_post_graphql_request);
|
||||||
|
|
||||||
|
let handle_get_request = move |context: Context, mut qry: HashMap<String, String>| {
|
||||||
|
let schema = schema.clone();
|
||||||
|
async move {
|
||||||
|
let res = task::spawn_blocking(move || {
|
||||||
|
let req = GraphQLRequest::new(
|
||||||
|
qry.remove("query").ok_or_else(|| {
|
||||||
|
failure::format_err!("Missing GraphQL query string in query parameters")
|
||||||
|
})?,
|
||||||
|
qry.remove("operation_name"),
|
||||||
|
qry.remove("variables")
|
||||||
|
.map(|vs| serde_json::from_str(&vs))
|
||||||
|
.transpose()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = req.execute_sync(&schema, &context);
|
||||||
|
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(build_response(res))
|
||||||
|
}
|
||||||
|
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
|
||||||
|
};
|
||||||
let get_filter = warp::get()
|
let get_filter = warp::get()
|
||||||
.and(context_extractor)
|
.and(context_extractor)
|
||||||
.and(warp::filters::query::query())
|
.and(query::query())
|
||||||
.and_then(handle_get_request);
|
.and_then(handle_get_request);
|
||||||
|
|
||||||
get_filter.or(post_filter).unify().boxed()
|
get_filter
|
||||||
|
.or(post_json_filter)
|
||||||
|
.unify()
|
||||||
|
.or(post_graphql_filter)
|
||||||
|
.unify()
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error raised by `tokio_threadpool` if the thread pool
|
/// Error raised by `tokio_threadpool` if the thread pool has been shutdown.
|
||||||
/// has been shutdown
|
|
||||||
///
|
///
|
||||||
/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`
|
/// Wrapper type is needed as inner type does not implement `warp::reject::Reject`.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct JoinError(task::JoinError);
|
pub struct JoinError(task::JoinError);
|
||||||
|
|
||||||
impl warp::reject::Reject for JoinError {}
|
impl warp::reject::Reject for JoinError {}
|
||||||
|
|
||||||
impl std::fmt::Debug for JoinError {
|
fn build_response(response: Result<(Vec<u8>, bool), failure::Error>) -> http::Response<Vec<u8>> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "JoinError({:?})", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_response(
|
|
||||||
response: Result<(Vec<u8>, bool), failure::Error>,
|
|
||||||
) -> warp::http::Response<Vec<u8>> {
|
|
||||||
match response {
|
match response {
|
||||||
Ok((body, is_ok)) => warp::http::Response::builder()
|
Ok((body, is_ok)) => http::Response::builder()
|
||||||
.status(if is_ok { 200 } else { 400 })
|
.status(if is_ok { 200 } else { 400 })
|
||||||
.header("content-type", "application/json")
|
.header("content-type", "application/json")
|
||||||
.body(body)
|
.body(body)
|
||||||
.expect("response is valid"),
|
.expect("response is valid"),
|
||||||
Err(_) => warp::http::Response::builder()
|
Err(_) => http::Response::builder()
|
||||||
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
|
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.body(Vec::new())
|
.body(Vec::new())
|
||||||
.expect("status code is valid"),
|
.expect("status code is valid"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response = Pin<
|
|
||||||
Box<dyn Future<Output = Result<warp::http::Response<Vec<u8>>, warp::reject::Rejection>> + Send>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint.
|
/// Create a filter that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint.
|
||||||
///
|
///
|
||||||
/// For example:
|
/// For example:
|
||||||
|
@ -318,7 +358,7 @@ type Response = Pin<
|
||||||
pub fn graphiql_filter(
|
pub fn graphiql_filter(
|
||||||
graphql_endpoint_url: &'static str,
|
graphql_endpoint_url: &'static str,
|
||||||
subscriptions_endpoint: Option<&'static str>,
|
subscriptions_endpoint: Option<&'static str>,
|
||||||
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
|
||||||
warp::any()
|
warp::any()
|
||||||
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
|
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -327,8 +367,8 @@ pub fn graphiql_filter(
|
||||||
fn graphiql_response(
|
fn graphiql_response(
|
||||||
graphql_endpoint_url: &'static str,
|
graphql_endpoint_url: &'static str,
|
||||||
subscriptions_endpoint: Option<&'static str>,
|
subscriptions_endpoint: Option<&'static str>,
|
||||||
) -> warp::http::Response<Vec<u8>> {
|
) -> http::Response<Vec<u8>> {
|
||||||
warp::http::Response::builder()
|
http::Response::builder()
|
||||||
.header("content-type", "text/html;charset=utf-8")
|
.header("content-type", "text/html;charset=utf-8")
|
||||||
.body(
|
.body(
|
||||||
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
|
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
|
||||||
|
@ -341,7 +381,7 @@ fn graphiql_response(
|
||||||
pub fn playground_filter(
|
pub fn playground_filter(
|
||||||
graphql_endpoint_url: &'static str,
|
graphql_endpoint_url: &'static str,
|
||||||
subscriptions_endpoint_url: Option<&'static str>,
|
subscriptions_endpoint_url: Option<&'static str>,
|
||||||
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
|
||||||
warp::any()
|
warp::any()
|
||||||
.map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
|
.map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -350,8 +390,8 @@ pub fn playground_filter(
|
||||||
fn playground_response(
|
fn playground_response(
|
||||||
graphql_endpoint_url: &'static str,
|
graphql_endpoint_url: &'static str,
|
||||||
subscriptions_endpoint_url: Option<&'static str>,
|
subscriptions_endpoint_url: Option<&'static str>,
|
||||||
) -> warp::http::Response<Vec<u8>> {
|
) -> http::Response<Vec<u8>> {
|
||||||
warp::http::Response::builder()
|
http::Response::builder()
|
||||||
.header("content-type", "text/html;charset=utf-8")
|
.header("content-type", "text/html;charset=utf-8")
|
||||||
.body(
|
.body(
|
||||||
juniper::http::playground::playground_source(
|
juniper::http::playground::playground_source(
|
||||||
|
@ -780,100 +820,122 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: update warp tests
|
#[cfg(test)]
|
||||||
//#[cfg(test)]
|
mod tests_http_harness {
|
||||||
//mod tests_http_harness {
|
use super::*;
|
||||||
// use super::*;
|
use juniper::{
|
||||||
// use juniper::{
|
http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
|
||||||
// http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
|
tests::{model::Database, schema::Query},
|
||||||
// tests::{model::Database, schema::Query},
|
EmptyMutation, EmptySubscription, RootNode,
|
||||||
// EmptyMutation, EmptySubscription, RootNode,
|
};
|
||||||
// };
|
use warp::{
|
||||||
// use warp::{self, Filter};
|
self,
|
||||||
//
|
filters::{path, BoxedFilter},
|
||||||
// type Schema =
|
Filter,
|
||||||
// juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
|
};
|
||||||
//
|
|
||||||
// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
struct TestWarpIntegration {
|
||||||
// let schema: Schema = RootNode::new(
|
filter: BoxedFilter<(http::Response<Vec<u8>>,)>,
|
||||||
// Query,
|
}
|
||||||
// EmptyMutation::<Database>::new(),
|
|
||||||
// EmptySubscription::<Database>::new(),
|
impl TestWarpIntegration {
|
||||||
// );
|
fn new(is_sync: bool) -> Self {
|
||||||
//
|
let schema = RootNode::new(
|
||||||
// let state = warp::any().map(move || Database::new());
|
Query,
|
||||||
// let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed()));
|
EmptyMutation::<Database>::new(),
|
||||||
//
|
EmptySubscription::<Database>::new(),
|
||||||
// filter.boxed()
|
);
|
||||||
// }
|
let state = warp::any().map(move || Database::new());
|
||||||
//
|
|
||||||
// struct TestWarpIntegration {
|
let filter = path::end().and(if is_sync {
|
||||||
// filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
|
make_graphql_filter_sync(schema, state.boxed())
|
||||||
// }
|
} else {
|
||||||
//
|
make_graphql_filter(schema, state.boxed())
|
||||||
// // This can't be implemented with the From trait since TestResponse is not defined in this crate.
|
});
|
||||||
// fn test_response_from_http_response(response: warp::http::Response<Vec<u8>>) -> TestResponse {
|
Self {
|
||||||
// TestResponse {
|
filter: filter.boxed(),
|
||||||
// status_code: response.status().as_u16() as i32,
|
}
|
||||||
// body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
|
}
|
||||||
// content_type: response
|
|
||||||
// .headers()
|
fn make_request(&self, req: warp::test::RequestBuilder) -> TestResponse {
|
||||||
// .get("content-type")
|
let mut rt = tokio::runtime::Runtime::new().expect("Failed to create tokio::Runtime");
|
||||||
// .expect("missing content-type header in warp response")
|
make_test_response(rt.block_on(async move {
|
||||||
// .to_str()
|
req.filter(&self.filter).await.unwrap_or_else(|rejection| {
|
||||||
// .expect("invalid content-type string")
|
let code = if rejection.is_not_found() {
|
||||||
// .to_owned(),
|
http::StatusCode::NOT_FOUND
|
||||||
// }
|
} else if let Some(body::BodyDeserializeError { .. }) = rejection.find() {
|
||||||
// }
|
http::StatusCode::BAD_REQUEST
|
||||||
//
|
} else {
|
||||||
// impl HTTPIntegration for TestWarpIntegration {
|
http::StatusCode::INTERNAL_SERVER_ERROR
|
||||||
// fn get(&self, url: &str) -> TestResponse {
|
};
|
||||||
// use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
|
http::Response::builder()
|
||||||
// let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
|
.status(code)
|
||||||
// .into_iter()
|
.header("content-type", "application/json")
|
||||||
// .collect::<Vec<_>>()
|
.body(Vec::new())
|
||||||
// .join("");
|
.unwrap()
|
||||||
//
|
})
|
||||||
// let response = warp::test::request()
|
}))
|
||||||
// .method("GET")
|
}
|
||||||
// .path(&format!("/?{}", url))
|
}
|
||||||
// .filter(&self.filter)
|
|
||||||
// .await
|
impl HttpIntegration for TestWarpIntegration {
|
||||||
// .unwrap_or_else(|rejection| {
|
fn get(&self, url: &str) -> TestResponse {
|
||||||
// warp::http::Response::builder()
|
use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};
|
||||||
// .status(rejection.status())
|
|
||||||
// .header("content-type", "application/json")
|
let url: String = utf8_percent_encode(&url.replace("/?", ""), QUERY_ENCODE_SET)
|
||||||
// .body(Vec::new())
|
.into_iter()
|
||||||
// .unwrap()
|
.collect::<Vec<_>>()
|
||||||
// });
|
.join("");
|
||||||
// test_response_from_http_response(response)
|
|
||||||
// }
|
self.make_request(
|
||||||
//
|
warp::test::request()
|
||||||
// fn post(&self, url: &str, body: &str) -> TestResponse {
|
.method("GET")
|
||||||
// let response = warp::test::request()
|
.path(&format!("/?{}", url)),
|
||||||
// .method("POST")
|
)
|
||||||
// .header("content-type", "application/json")
|
}
|
||||||
// .path(url)
|
|
||||||
// .body(body)
|
fn post_json(&self, url: &str, body: &str) -> TestResponse {
|
||||||
// .filter(&self.filter)
|
self.make_request(
|
||||||
// .await
|
warp::test::request()
|
||||||
// .unwrap_or_else(|rejection| {
|
.method("POST")
|
||||||
// warp::http::Response::builder()
|
.header("content-type", "application/json")
|
||||||
// .status(rejection.status())
|
.path(url)
|
||||||
// .header("content-type", "application/json")
|
.body(body),
|
||||||
// .body(Vec::new())
|
)
|
||||||
// .unwrap()
|
}
|
||||||
// });
|
|
||||||
// test_response_from_http_response(response)
|
fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
|
||||||
// }
|
self.make_request(
|
||||||
// }
|
warp::test::request()
|
||||||
//
|
.method("POST")
|
||||||
// #[test]
|
.header("content-type", "application/graphql")
|
||||||
// fn test_warp_integration() {
|
.path(url)
|
||||||
// let integration = TestWarpIntegration {
|
.body(body),
|
||||||
// filter: warp_server(),
|
)
|
||||||
// };
|
}
|
||||||
//
|
}
|
||||||
// run_http_test_suite(&integration);
|
|
||||||
// }
|
fn make_test_response(resp: http::Response<Vec<u8>>) -> TestResponse {
|
||||||
//}
|
TestResponse {
|
||||||
|
status_code: resp.status().as_u16() as i32,
|
||||||
|
body: Some(String::from_utf8(resp.body().to_owned()).unwrap()),
|
||||||
|
content_type: resp
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.expect("missing content-type header in warp response")
|
||||||
|
.to_str()
|
||||||
|
.expect("invalid content-type string")
|
||||||
|
.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_warp_integration() {
|
||||||
|
run_http_test_suite(&TestWarpIntegration::new(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_warp_integration() {
|
||||||
|
run_http_test_suite(&TestWarpIntegration::new(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue