* 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")))]`
|
||||
(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)
|
||||
|
||||
- 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,
|
||||
}
|
||||
|
||||
/// Normalized way to make requests to the http framework
|
||||
/// integration we are testing.
|
||||
pub trait HTTPIntegration {
|
||||
/// Normalized way to make requests to the HTTP framework integration we are testing.
|
||||
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 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)]
|
||||
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!(" - test_simple_get");
|
||||
|
@ -404,6 +412,12 @@ pub mod tests {
|
|||
|
||||
println!(" - test_duplicate_keys");
|
||||
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 {
|
||||
|
@ -416,7 +430,7 @@ pub mod tests {
|
|||
.expect("Could not parse JSON object")
|
||||
}
|
||||
|
||||
fn test_simple_get<T: HTTPIntegration>(integration: &T) {
|
||||
fn test_simple_get<T: HttpIntegration>(integration: &T) {
|
||||
// {hero{name}}
|
||||
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 } }
|
||||
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");
|
||||
|
@ -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 } }
|
||||
// with variables = { "id": "1000" }
|
||||
let response = integration.get(
|
||||
|
@ -491,8 +505,8 @@ pub mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
fn test_simple_post<T: HTTPIntegration>(integration: &T) {
|
||||
let response = integration.post("/", r#"{"query": "{hero{name}}"}"#);
|
||||
fn test_simple_post<T: HttpIntegration>(integration: &T) {
|
||||
let response = integration.post_json("/", r#"{"query": "{hero{name}}"}"#);
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(response.content_type, "application/json");
|
||||
|
@ -500,12 +514,12 @@ pub mod tests {
|
|||
assert_eq!(
|
||||
unwrap_json_response(&response),
|
||||
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) {
|
||||
let response = integration.post(
|
||||
fn test_batched_post<T: HttpIntegration>(integration: &T) {
|
||||
let response = integration.post_json(
|
||||
"/",
|
||||
r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#,
|
||||
);
|
||||
|
@ -516,42 +530,57 @@ pub mod tests {
|
|||
assert_eq!(
|
||||
unwrap_json_response(&response),
|
||||
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) {
|
||||
let response = integration.post("/", "[]");
|
||||
fn test_empty_batched_post<T: HttpIntegration>(integration: &T) {
|
||||
let response = integration.post_json("/", "[]");
|
||||
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");
|
||||
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);
|
||||
}
|
||||
|
||||
fn test_invalid_field<T: HTTPIntegration>(integration: &T) {
|
||||
fn test_invalid_field<T: HttpIntegration>(integration: &T) {
|
||||
// {hero{blah}}
|
||||
let response = integration.get("/?query=%7Bhero%7Bblah%7D%7D");
|
||||
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);
|
||||
}
|
||||
|
||||
fn test_duplicate_keys<T: HTTPIntegration>(integration: &T) {
|
||||
fn test_duplicate_keys<T: HttpIntegration>(integration: &T) {
|
||||
// {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");
|
||||
assert_eq!(response.status_code, 400);
|
||||
let response = integration.post(
|
||||
"/",
|
||||
r#"
|
||||
{"query": "{hero{name}}", "query": "{hero{name}}"}
|
||||
"#,
|
||||
);
|
||||
let response =
|
||||
integration.post_json("/", r#"{"query": "{hero{name}}", "query": "{hero{name}}"}"#);
|
||||
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 gql_response = req.execute(schema, context).await;
|
||||
let body_response = serde_json::to_string(&gql_response)?;
|
||||
let response = match gql_response.is_ok() {
|
||||
true => HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body_response),
|
||||
false => HttpResponse::BadRequest()
|
||||
.content_type("application/json")
|
||||
.body(body_response),
|
||||
let mut response = match gql_response.is_ok() {
|
||||
true => HttpResponse::Ok(),
|
||||
false => HttpResponse::BadRequest(),
|
||||
};
|
||||
Ok(response)
|
||||
Ok(response
|
||||
.content_type("application/json")
|
||||
.body(body_response))
|
||||
}
|
||||
|
||||
/// Actix GraphQL Handler for POST requests
|
||||
|
@ -160,13 +158,15 @@ where
|
|||
.get(CONTENT_TYPE)
|
||||
.and_then(|hv| hv.to_str().ok());
|
||||
let req = match content_type_header {
|
||||
Some("application/json") | Some("application/graphql") => {
|
||||
let body_string = String::from_request(&req, &mut payload.into_inner()).await;
|
||||
let body_string = body_string?;
|
||||
match serde_json::from_str::<GraphQLBatchRequest<S>>(&body_string) {
|
||||
Ok(req) => Ok(req),
|
||||
Err(err) => Err(ErrorBadRequest(err)),
|
||||
Some("application/json") => {
|
||||
let body = String::from_request(&req, &mut payload.into_inner()).await?;
|
||||
serde_json::from_str::<GraphQLBatchRequest<S>>(&body).map_err(ErrorBadRequest)
|
||||
}
|
||||
Some("application/graphql") => {
|
||||
let body = String::from_request(&req, &mut payload.into_inner()).await?;
|
||||
Ok(GraphQLBatchRequest::Single(GraphQLRequest::new(
|
||||
body, None, None,
|
||||
)))
|
||||
}
|
||||
_ => Err(ErrorUnsupportedMediaType(
|
||||
"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 futures::StreamExt;
|
||||
use juniper::{
|
||||
http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
|
||||
http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
|
||||
tests::{model::Database, schema::Query},
|
||||
EmptyMutation, EmptySubscription, RootNode,
|
||||
};
|
||||
|
@ -446,63 +446,54 @@ mod tests {
|
|||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
pub struct TestActixWebIntegration {}
|
||||
pub struct TestActixWebIntegration;
|
||||
|
||||
impl HTTPIntegration for TestActixWebIntegration {
|
||||
impl TestActixWebIntegration {
|
||||
fn make_request(&self, req: test::TestRequest) -> TestResponse {
|
||||
actix_rt::System::new("request").block_on(async move {
|
||||
let schema = RootNode::new(
|
||||
Query,
|
||||
EmptyMutation::<Database>::new(),
|
||||
EmptySubscription::<Database>::new(),
|
||||
);
|
||||
|
||||
let mut app =
|
||||
test::init_service(App::new().data(schema).route("/", web::to(index))).await;
|
||||
|
||||
let resp = test::call_service(&mut app, req.to_request()).await;
|
||||
make_test_response(resp).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpIntegration for TestActixWebIntegration {
|
||||
fn get(&self, url: &str) -> TestResponse {
|
||||
let url = url.to_string();
|
||||
actix_rt::System::new("get_request").block_on(async move {
|
||||
let schema: Schema = RootNode::new(
|
||||
Query,
|
||||
EmptyMutation::<Database>::new(),
|
||||
EmptySubscription::<Database>::new(),
|
||||
);
|
||||
let req = test::TestRequest::get()
|
||||
self.make_request(test::TestRequest::get().uri(url))
|
||||
}
|
||||
|
||||
fn post_json(&self, url: &str, body: &str) -> TestResponse {
|
||||
self.make_request(
|
||||
test::TestRequest::post()
|
||||
.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
|
||||
})
|
||||
.set_payload(body.to_string())
|
||||
.uri(url),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
EmptyMutation::<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 =
|
||||
test::init_service(App::new().data(schema).route("/", web::post().to(index)))
|
||||
.await;
|
||||
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
let test_response = make_test_response(resp).await;
|
||||
test_response
|
||||
})
|
||||
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 response: ServiceResponse) -> TestResponse {
|
||||
let body = take_response_body_string(&mut response).await;
|
||||
let status_code = response.status().as_u16();
|
||||
let content_type = response.headers().get(CONTENT_TYPE).unwrap();
|
||||
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 {
|
||||
status_code: status_code as i32,
|
||||
body: Some(body),
|
||||
|
@ -512,6 +503,6 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_actix_web_integration() {
|
||||
run_http_test_suite(&TestActixWebIntegration {});
|
||||
run_http_test_suite(&TestActixWebIntegration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
## 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)
|
||||
|
||||
|
|
|
@ -8,17 +8,17 @@ use hyper::{
|
|||
Body, Method, Request, Response, StatusCode,
|
||||
};
|
||||
use juniper::{
|
||||
http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest},
|
||||
http::{GraphQLBatchRequest, GraphQLRequest as JuniperGraphQLRequest, GraphQLRequest},
|
||||
GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, InputValue, RootNode, ScalarValue,
|
||||
};
|
||||
use serde_json::error::Error as SerdeError;
|
||||
use std::{error::Error, fmt, string::FromUtf8Error, sync::Arc};
|
||||
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>>,
|
||||
context: Arc<CtxT>,
|
||||
request: Request<Body>,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, hyper::Error>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
|
@ -30,31 +30,16 @@ where
|
|||
MutationT::TypeInfo: Send + Sync,
|
||||
SubscriptionT::TypeInfo: Send + Sync,
|
||||
{
|
||||
match *request.method() {
|
||||
Method::GET => {
|
||||
let gql_req = parse_get_req(request);
|
||||
|
||||
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)),
|
||||
}
|
||||
Ok(match parse_req(req).await {
|
||||
Ok(req) => execute_request_sync(root_node, context, req).await,
|
||||
Err(resp) => resp,
|
||||
})
|
||||
}
|
||||
|
||||
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>>,
|
||||
context: Arc<CtxT>,
|
||||
request: Request<Body>,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, hyper::Error>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
|
@ -66,25 +51,31 @@ where
|
|||
MutationT::TypeInfo: Send + Sync,
|
||||
SubscriptionT::TypeInfo: Send + Sync,
|
||||
{
|
||||
match *request.method() {
|
||||
Method::GET => {
|
||||
let gql_req = parse_get_req(request);
|
||||
Ok(match parse_req(req).await {
|
||||
Ok(req) => execute_request(root_node, context, req).await,
|
||||
Err(resp) => resp,
|
||||
})
|
||||
}
|
||||
|
||||
match gql_req {
|
||||
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
|
||||
Err(err) => Ok(render_error(err)),
|
||||
}
|
||||
}
|
||||
async fn parse_req<S: ScalarValue>(
|
||||
req: Request<Body>,
|
||||
) -> Result<GraphQLBatchRequest<S>, Response<Body>> {
|
||||
match *req.method() {
|
||||
Method::GET => parse_get_req(req),
|
||||
Method::POST => {
|
||||
let gql_req = parse_post_req(request.into_body()).await;
|
||||
|
||||
match gql_req {
|
||||
Ok(gql_req) => Ok(execute_request_async(root_node, context, gql_req).await),
|
||||
Err(err) => Ok(render_error(err)),
|
||||
let content_type = req
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.map(HeaderValue::to_str);
|
||||
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>(
|
||||
|
@ -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,
|
||||
) -> Result<GraphQLBatchRequest<S>, GraphQLRequestError> {
|
||||
let chunk = hyper::body::to_bytes(body)
|
||||
|
@ -114,6 +105,21 @@ async fn parse_post_req<S: ScalarValue>(
|
|||
.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(
|
||||
graphql_endpoint: &str,
|
||||
subscriptions_endpoint: Option<&str>,
|
||||
|
@ -146,7 +152,7 @@ fn render_error(err: GraphQLRequestError) -> Response<Body> {
|
|||
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>>,
|
||||
context: Arc<CtxT>,
|
||||
request: GraphQLBatchRequest<S>,
|
||||
|
@ -177,7 +183,7 @@ where
|
|||
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>>,
|
||||
context: Arc<CtxT>,
|
||||
request: GraphQLBatchRequest<S>,
|
||||
|
@ -319,19 +325,34 @@ mod tests {
|
|||
use reqwest::{self, Response as ReqwestResponse};
|
||||
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 {
|
||||
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)))
|
||||
}
|
||||
|
||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||
let url = format!("http://127.0.0.1:3001/graphql{}", url);
|
||||
fn post_json(&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/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())
|
||||
.send()
|
||||
.expect(&format!("failed POST {}", url));
|
||||
|
@ -356,9 +377,9 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hyper_integration() {
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], 3001).into();
|
||||
async fn run_hyper_integration(is_sync: bool) {
|
||||
let port = if is_sync { 3002 } else { 3001 };
|
||||
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
|
||||
|
||||
let db = Arc::new(Database::new());
|
||||
let root_node = Arc::new(RootNode::new(
|
||||
|
@ -386,11 +407,15 @@ mod tests {
|
|||
};
|
||||
async move {
|
||||
if matches {
|
||||
super::graphql(root_node, ctx, req).await
|
||||
if is_sync {
|
||||
super::graphql_sync(root_node, ctx, req).await
|
||||
} else {
|
||||
let mut response = Response::new(Body::empty());
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(response)
|
||||
super::graphql(root_node, ctx, req).await
|
||||
}
|
||||
} else {
|
||||
let mut resp = Response::new(Body::empty());
|
||||
*resp.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -409,7 +434,7 @@ mod tests {
|
|||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
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);
|
||||
shutdown.abort();
|
||||
});
|
||||
|
@ -418,4 +443,14 @@ mod tests {
|
|||
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
|
||||
|
||||
- `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)
|
||||
|
||||
|
|
|
@ -114,10 +114,17 @@ extern crate iron_test;
|
|||
#[cfg(test)]
|
||||
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 std::{error::Error, fmt, io::Read};
|
||||
use std::{error::Error, fmt, io::Read, ops::Deref as _};
|
||||
|
||||
use serde_json::error::Error as SerdeError;
|
||||
|
||||
|
@ -228,32 +235,41 @@ where
|
|||
}
|
||||
|
||||
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||
let url_query_string = req
|
||||
let url_query = req
|
||||
.get_mut::<UrlEncodedQuery>()
|
||||
.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"))?;
|
||||
let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
|
||||
let variables = parse_variable_param(url_query_string.remove("variables"))?;
|
||||
let operation_name = parse_url_param(url_query.remove("operationName"))?;
|
||||
let variables = parse_variable_param(url_query.remove("variables"))?;
|
||||
|
||||
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||
input_query,
|
||||
query,
|
||||
operation_name,
|
||||
variables,
|
||||
)))
|
||||
}
|
||||
|
||||
fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||
let mut request_payload = String::new();
|
||||
itry!(req.body.read_to_string(&mut request_payload));
|
||||
fn handle_post_json(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
|
||||
let mut payload = String::new();
|
||||
itry!(req.body.read_to_string(&mut payload));
|
||||
|
||||
Ok(
|
||||
serde_json::from_str::<GraphQLBatchRequest<S>>(request_payload.as_str())
|
||||
serde_json::from_str::<GraphQLBatchRequest<S>>(payload.as_str())
|
||||
.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(
|
||||
&self,
|
||||
context: &CtxT,
|
||||
|
@ -313,7 +329,14 @@ where
|
|||
|
||||
let graphql_request = match req.method {
|
||||
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)),
|
||||
};
|
||||
|
||||
|
@ -388,7 +411,11 @@ impl From<GraphQLIronError> for IronError {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use iron::{Handler, Headers, Url};
|
||||
use iron::{
|
||||
headers::ContentType,
|
||||
mime::{Mime, SubLevel, TopLevel},
|
||||
Handler, Headers, Url,
|
||||
};
|
||||
use iron_test::{request, response};
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
|
@ -411,7 +438,7 @@ mod tests {
|
|||
.path()
|
||||
.iter()
|
||||
.map(|x| (*x).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
format!(
|
||||
"http://localhost:3000{}?{}",
|
||||
|
@ -422,21 +449,31 @@ mod tests {
|
|||
|
||||
struct TestIronIntegration;
|
||||
|
||||
impl http_tests::HTTPIntegration for TestIronIntegration {
|
||||
impl http_tests::HttpIntegration for TestIronIntegration {
|
||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||
let result = request::get(&fixup_url(url), Headers::new(), &make_handler());
|
||||
match result {
|
||||
Ok(response) => make_test_response(response),
|
||||
Err(e) => make_test_error_response(e),
|
||||
}
|
||||
request::get(&fixup_url(url), Headers::new(), &make_handler())
|
||||
.map(make_test_response)
|
||||
.unwrap_or_else(make_test_error_response)
|
||||
}
|
||||
|
||||
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||
let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler());
|
||||
match result {
|
||||
Ok(response) => make_test_response(response),
|
||||
Err(e) => make_test_error_response(e),
|
||||
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(ContentType::json());
|
||||
request::post(&fixup_url(url), headers, body, &make_handler())
|
||||
.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`.
|
||||
- Rocket integration does not require default features.
|
||||
- Support `application/graphql` POST requests.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ use rocket::{
|
|||
request::{FormItems, FromForm, FromFormValue},
|
||||
response::{content, Responder, Response},
|
||||
Data,
|
||||
Outcome::{Failure, Forward, Success},
|
||||
Outcome::{Forward, Success},
|
||||
Request,
|
||||
};
|
||||
|
||||
|
@ -271,20 +271,26 @@ where
|
|||
{
|
||||
type Error = String;
|
||||
|
||||
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
|
||||
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
||||
return Forward(data);
|
||||
}
|
||||
fn from_data(req: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
|
||||
let content_type = req
|
||||
.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();
|
||||
if let Err(e) = data.open().read_to_string(&mut body) {
|
||||
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
||||
}
|
||||
data.open()
|
||||
.read_to_string(&mut body)
|
||||
.map_err(|e| Err((Status::InternalServerError, format!("{:?}", e))))?;
|
||||
|
||||
match serde_json::from_str(&body) {
|
||||
Ok(value) => Success(GraphQLRequest(value)),
|
||||
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
|
||||
}
|
||||
Success(GraphQLRequest(if is_json {
|
||||
serde_json::from_str(&body).map_err(|e| Err((Status::BadRequest, format!("{}", e))))?
|
||||
} else {
|
||||
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -456,16 +462,25 @@ mod tests {
|
|||
client: Client,
|
||||
}
|
||||
|
||||
impl http_tests::HTTPIntegration for TestRocketIntegration {
|
||||
impl http_tests::HttpIntegration for TestRocketIntegration {
|
||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||
let req = &self.client.get(url);
|
||||
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);
|
||||
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]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
- Compatibility with the latest `juniper`.
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -294,12 +294,17 @@ where
|
|||
{
|
||||
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 _;
|
||||
|
||||
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
||||
return Box::pin(async move { Forward(data) });
|
||||
}
|
||||
let content_type = req
|
||||
.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 {
|
||||
let mut body = String::new();
|
||||
|
@ -308,10 +313,14 @@ where
|
|||
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
||||
}
|
||||
|
||||
Success(GraphQLRequest(if is_json {
|
||||
match serde_json::from_str(&body) {
|
||||
Ok(value) => Success(GraphQLRequest(value)),
|
||||
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,
|
||||
}
|
||||
|
||||
impl http_tests::HTTPIntegration for TestRocketIntegration {
|
||||
impl http_tests::HttpIntegration for TestRocketIntegration {
|
||||
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||
let req = self.client.get(url);
|
||||
let req = futures::executor::block_on(req.dispatch());
|
||||
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 = futures::executor::block_on(req.dispatch());
|
||||
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]
|
||||
|
|
|
@ -6,10 +6,11 @@ to `juniper` to be reused in other http integrations, since this implementation
|
|||
|
||||
## Breaking Changes
|
||||
|
||||
- Update `playground_filter` to support subscription endpoint URLs
|
||||
- Update `warp` to 0.2
|
||||
- Update `playground_filter` to support subscription endpoint URLs.
|
||||
- Update `warp` to 0.2.
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -12,18 +12,19 @@ edition = "2018"
|
|||
subscriptions = ["juniper_subscriptions"]
|
||||
|
||||
[dependencies]
|
||||
warp = "0.2"
|
||||
bytes = "0.5"
|
||||
failure = "0.1.7"
|
||||
futures = "0.3.1"
|
||||
juniper = { version = "0.14.2", path = "../juniper", default-features = false }
|
||||
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true}
|
||||
tokio = { version = "0.2", features = ["rt-core", "blocking"] }
|
||||
juniper_subscriptions = { path = "../juniper_subscriptions", optional = true }
|
||||
serde = { version = "1.0.75", features = ["derive"] }
|
||||
serde_json = "1.0.24"
|
||||
failure = "0.1.7"
|
||||
tokio = { version = "0.2", features = ["blocking", "rt-core"] }
|
||||
warp = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
|
||||
env_logger = "0.5.11"
|
||||
juniper = { version = "0.14.2", path = "../juniper", features = ["expose-test-schema", "serde_json"] }
|
||||
log = "0.4.3"
|
||||
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)]
|
||||
#![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 juniper::{http::GraphQLBatchRequest, ScalarValue};
|
||||
use bytes::Bytes;
|
||||
use futures::{FutureExt as _, TryFutureExt};
|
||||
use juniper::{
|
||||
http::{GraphQLBatchRequest, GraphQLRequest},
|
||||
ScalarValue,
|
||||
};
|
||||
use tokio::task;
|
||||
use warp::{filters::BoxedFilter, Filter};
|
||||
use warp::{body, filters::BoxedFilter, header, http, query, Filter};
|
||||
|
||||
/// 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>(
|
||||
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||
context_extractor: BoxedFilter<(Context,)>,
|
||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
||||
) -> BoxedFilter<(http::Response<Vec<u8>>,)>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
Context: Send + Sync + 'static,
|
||||
|
@ -122,65 +126,90 @@ where
|
|||
Subscription::TypeInfo: Send + Sync,
|
||||
{
|
||||
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 schema = post_schema.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let res = request.execute(&schema, &context).await;
|
||||
let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
|
||||
let schema = post_json_schema.clone();
|
||||
async move {
|
||||
let resp = req.execute(&schema, &context).await;
|
||||
|
||||
Ok::<_, warp::Rejection>(build_response(
|
||||
serde_json::to_vec(&res)
|
||||
.map(|json| (json, res.is_ok()))
|
||||
serde_json::to_vec(&resp)
|
||||
.map(|json| (json, resp.is_ok()))
|
||||
.map_err(Into::into),
|
||||
))
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let post_filter = warp::post()
|
||||
let post_json_filter = warp::post()
|
||||
.and(header::exact_ignore_case(
|
||||
"content-type",
|
||||
"application/json",
|
||||
))
|
||||
.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>| {
|
||||
let schema = schema.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 variables = match request.remove("variables") {
|
||||
None => None,
|
||||
Some(vs) => serde_json::from_str(&vs)?,
|
||||
};
|
||||
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);
|
||||
|
||||
let graphql_request = juniper::http::GraphQLRequest::new(
|
||||
request.remove("query").ok_or_else(|| {
|
||||
let resp = req.execute(&schema, &context).await;
|
||||
|
||||
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||
}
|
||||
.then(|res| async { Ok::<_, warp::Rejection>(build_response(res)) })
|
||||
};
|
||||
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 req = GraphQLRequest::new(
|
||||
qry.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,
|
||||
qry.remove("operation_name"),
|
||||
qry.remove("variables")
|
||||
.map(|vs| serde_json::from_str(&vs))
|
||||
.transpose()?,
|
||||
);
|
||||
|
||||
let response = graphql_request.execute(&schema, &context).await;
|
||||
let resp = req.execute(&schema, &context).await;
|
||||
|
||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
||||
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||
}
|
||||
.then(|result| async move { Ok::<_, warp::Rejection>(build_response(result)) })
|
||||
.then(|res| async move { Ok::<_, warp::Rejection>(build_response(res)) })
|
||||
};
|
||||
|
||||
let get_filter = warp::get()
|
||||
.and(context_extractor)
|
||||
.and(warp::filters::query::query())
|
||||
.and(query::query())
|
||||
.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.
|
||||
pub fn make_graphql_filter_sync<Query, Mutation, Subscription, Context, S>(
|
||||
schema: juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||
context_extractor: BoxedFilter<(Context,)>,
|
||||
) -> BoxedFilter<(warp::http::Response<Vec<u8>>,)>
|
||||
) -> BoxedFilter<(http::Response<Vec<u8>>,)>
|
||||
where
|
||||
S: ScalarValue + Send + Sync + 'static,
|
||||
Context: Send + Sync + 'static,
|
||||
|
@ -189,105 +218,116 @@ where
|
|||
Subscription: juniper::GraphQLType<S, Context = Context, TypeInfo = ()> + Send + Sync + 'static,
|
||||
{
|
||||
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>| -> Response {
|
||||
let schema = post_schema.clone();
|
||||
|
||||
Box::pin(
|
||||
let handle_post_json_request = move |context: Context, req: GraphQLBatchRequest<S>| {
|
||||
let schema = post_json_schema.clone();
|
||||
async move {
|
||||
let result = task::spawn_blocking(move || {
|
||||
let response = request.execute_sync(&schema, &context);
|
||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
||||
let res = task::spawn_blocking(move || {
|
||||
let resp = req.execute_sync(&schema, &context);
|
||||
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(build_response(result))
|
||||
Ok(build_response(res))
|
||||
}
|
||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
|
||||
)
|
||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
|
||||
};
|
||||
|
||||
let post_filter = warp::post()
|
||||
let post_json_filter = warp::post()
|
||||
.and(header::exact_ignore_case(
|
||||
"content-type",
|
||||
"application/json",
|
||||
))
|
||||
.and(context_extractor.clone())
|
||||
.and(warp::body::json())
|
||||
.and_then(handle_post_request);
|
||||
.and(body::json())
|
||||
.and_then(handle_post_json_request);
|
||||
|
||||
let handle_get_request = move |context: Context,
|
||||
mut request: std::collections::HashMap<String, String>|
|
||||
-> Response {
|
||||
let schema = schema.clone();
|
||||
|
||||
Box::pin(
|
||||
let handle_post_graphql_request = move |context: Context, body: Bytes| {
|
||||
let schema = post_graphql_schema.clone();
|
||||
async move {
|
||||
let result = task::spawn_blocking(move || {
|
||||
let variables = match request.remove("variables") {
|
||||
None => None,
|
||||
Some(vs) => serde_json::from_str(&vs)?,
|
||||
};
|
||||
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 graphql_request = juniper::http::GraphQLRequest::new(
|
||||
request.remove("query").ok_or_else(|| {
|
||||
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")
|
||||
})?,
|
||||
request.get("operation_name").map(|s| s.to_owned()),
|
||||
variables,
|
||||
qry.remove("operation_name"),
|
||||
qry.remove("variables")
|
||||
.map(|vs| serde_json::from_str(&vs))
|
||||
.transpose()?,
|
||||
);
|
||||
|
||||
let response = graphql_request.execute_sync(&schema, &context);
|
||||
Ok((serde_json::to_vec(&response)?, response.is_ok()))
|
||||
let resp = req.execute_sync(&schema, &context);
|
||||
Ok((serde_json::to_vec(&resp)?, resp.is_ok()))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(build_response(result))
|
||||
Ok(build_response(res))
|
||||
}
|
||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e))),
|
||||
)
|
||||
.map_err(|e: task::JoinError| warp::reject::custom(JoinError(e)))
|
||||
};
|
||||
|
||||
let get_filter = warp::get()
|
||||
.and(context_extractor)
|
||||
.and(warp::filters::query::query())
|
||||
.and(query::query())
|
||||
.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
|
||||
/// has been shutdown
|
||||
/// Error raised by `tokio_threadpool` if the thread pool 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);
|
||||
|
||||
impl warp::reject::Reject for JoinError {}
|
||||
|
||||
impl std::fmt::Debug for JoinError {
|
||||
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>> {
|
||||
fn build_response(response: Result<(Vec<u8>, bool), failure::Error>) -> http::Response<Vec<u8>> {
|
||||
match response {
|
||||
Ok((body, is_ok)) => warp::http::Response::builder()
|
||||
Ok((body, is_ok)) => http::Response::builder()
|
||||
.status(if is_ok { 200 } else { 400 })
|
||||
.header("content-type", "application/json")
|
||||
.body(body)
|
||||
.expect("response is valid"),
|
||||
Err(_) => warp::http::Response::builder()
|
||||
.status(warp::http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
Err(_) => http::Response::builder()
|
||||
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Vec::new())
|
||||
.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.
|
||||
///
|
||||
/// For example:
|
||||
|
@ -318,7 +358,7 @@ type Response = Pin<
|
|||
pub fn graphiql_filter(
|
||||
graphql_endpoint_url: &'static str,
|
||||
subscriptions_endpoint: Option<&'static str>,
|
||||
) -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
||||
) -> warp::filters::BoxedFilter<(http::Response<Vec<u8>>,)> {
|
||||
warp::any()
|
||||
.map(move || graphiql_response(graphql_endpoint_url, subscriptions_endpoint))
|
||||
.boxed()
|
||||
|
@ -327,8 +367,8 @@ pub fn graphiql_filter(
|
|||
fn graphiql_response(
|
||||
graphql_endpoint_url: &'static str,
|
||||
subscriptions_endpoint: Option<&'static str>,
|
||||
) -> warp::http::Response<Vec<u8>> {
|
||||
warp::http::Response::builder()
|
||||
) -> http::Response<Vec<u8>> {
|
||||
http::Response::builder()
|
||||
.header("content-type", "text/html;charset=utf-8")
|
||||
.body(
|
||||
juniper::http::graphiql::graphiql_source(graphql_endpoint_url, subscriptions_endpoint)
|
||||
|
@ -341,7 +381,7 @@ fn graphiql_response(
|
|||
pub fn playground_filter(
|
||||
graphql_endpoint_url: &'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()
|
||||
.map(move || playground_response(graphql_endpoint_url, subscriptions_endpoint_url))
|
||||
.boxed()
|
||||
|
@ -350,8 +390,8 @@ pub fn playground_filter(
|
|||
fn playground_response(
|
||||
graphql_endpoint_url: &'static str,
|
||||
subscriptions_endpoint_url: Option<&'static str>,
|
||||
) -> warp::http::Response<Vec<u8>> {
|
||||
warp::http::Response::builder()
|
||||
) -> http::Response<Vec<u8>> {
|
||||
http::Response::builder()
|
||||
.header("content-type", "text/html;charset=utf-8")
|
||||
.body(
|
||||
juniper::http::playground::playground_source(
|
||||
|
@ -780,100 +820,122 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
//TODO: update warp tests
|
||||
//#[cfg(test)]
|
||||
//mod tests_http_harness {
|
||||
// use super::*;
|
||||
// use juniper::{
|
||||
// http::tests::{run_http_test_suite, HTTPIntegration, TestResponse},
|
||||
// tests::{model::Database, schema::Query},
|
||||
// EmptyMutation, EmptySubscription, RootNode,
|
||||
// };
|
||||
// use warp::{self, Filter};
|
||||
//
|
||||
// type Schema =
|
||||
// juniper::RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
|
||||
//
|
||||
// fn warp_server() -> warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)> {
|
||||
// let schema: Schema = RootNode::new(
|
||||
// Query,
|
||||
// EmptyMutation::<Database>::new(),
|
||||
// EmptySubscription::<Database>::new(),
|
||||
// );
|
||||
//
|
||||
// let state = warp::any().map(move || Database::new());
|
||||
// let filter = warp::filters::path::end().and(make_graphql_filter(schema, state.boxed()));
|
||||
//
|
||||
// filter.boxed()
|
||||
// }
|
||||
//
|
||||
// struct TestWarpIntegration {
|
||||
// filter: warp::filters::BoxedFilter<(warp::http::Response<Vec<u8>>,)>,
|
||||
// }
|
||||
//
|
||||
// // 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 {
|
||||
// TestResponse {
|
||||
// status_code: response.status().as_u16() as i32,
|
||||
// body: Some(String::from_utf8(response.body().to_owned()).unwrap()),
|
||||
// content_type: response
|
||||
// .headers()
|
||||
// .get("content-type")
|
||||
// .expect("missing content-type header in warp response")
|
||||
// .to_str()
|
||||
// .expect("invalid content-type string")
|
||||
// .to_owned(),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl HTTPIntegration for TestWarpIntegration {
|
||||
// fn get(&self, url: &str) -> TestResponse {
|
||||
// use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
|
||||
// let url: String = percent_encode(url.replace("/?", "").as_bytes(), DEFAULT_ENCODE_SET)
|
||||
// .into_iter()
|
||||
// .collect::<Vec<_>>()
|
||||
// .join("");
|
||||
//
|
||||
// let response = warp::test::request()
|
||||
// .method("GET")
|
||||
// .path(&format!("/?{}", url))
|
||||
// .filter(&self.filter)
|
||||
// .await
|
||||
// .unwrap_or_else(|rejection| {
|
||||
// warp::http::Response::builder()
|
||||
// .status(rejection.status())
|
||||
// .header("content-type", "application/json")
|
||||
// .body(Vec::new())
|
||||
// .unwrap()
|
||||
// });
|
||||
// test_response_from_http_response(response)
|
||||
// }
|
||||
//
|
||||
// fn post(&self, url: &str, body: &str) -> TestResponse {
|
||||
// let response = warp::test::request()
|
||||
// .method("POST")
|
||||
// .header("content-type", "application/json")
|
||||
// .path(url)
|
||||
// .body(body)
|
||||
// .filter(&self.filter)
|
||||
// .await
|
||||
// .unwrap_or_else(|rejection| {
|
||||
// warp::http::Response::builder()
|
||||
// .status(rejection.status())
|
||||
// .header("content-type", "application/json")
|
||||
// .body(Vec::new())
|
||||
// .unwrap()
|
||||
// });
|
||||
// test_response_from_http_response(response)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_warp_integration() {
|
||||
// let integration = TestWarpIntegration {
|
||||
// filter: warp_server(),
|
||||
// };
|
||||
//
|
||||
// run_http_test_suite(&integration);
|
||||
// }
|
||||
//}
|
||||
#[cfg(test)]
|
||||
mod tests_http_harness {
|
||||
use super::*;
|
||||
use juniper::{
|
||||
http::tests::{run_http_test_suite, HttpIntegration, TestResponse},
|
||||
tests::{model::Database, schema::Query},
|
||||
EmptyMutation, EmptySubscription, RootNode,
|
||||
};
|
||||
use warp::{
|
||||
self,
|
||||
filters::{path, BoxedFilter},
|
||||
Filter,
|
||||
};
|
||||
|
||||
struct TestWarpIntegration {
|
||||
filter: BoxedFilter<(http::Response<Vec<u8>>,)>,
|
||||
}
|
||||
|
||||
impl TestWarpIntegration {
|
||||
fn new(is_sync: bool) -> Self {
|
||||
let schema = RootNode::new(
|
||||
Query,
|
||||
EmptyMutation::<Database>::new(),
|
||||
EmptySubscription::<Database>::new(),
|
||||
);
|
||||
let state = warp::any().map(move || Database::new());
|
||||
|
||||
let filter = path::end().and(if is_sync {
|
||||
make_graphql_filter_sync(schema, state.boxed())
|
||||
} else {
|
||||
make_graphql_filter(schema, state.boxed())
|
||||
});
|
||||
Self {
|
||||
filter: filter.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_request(&self, req: warp::test::RequestBuilder) -> TestResponse {
|
||||
let mut rt = tokio::runtime::Runtime::new().expect("Failed to create tokio::Runtime");
|
||||
make_test_response(rt.block_on(async move {
|
||||
req.filter(&self.filter).await.unwrap_or_else(|rejection| {
|
||||
let code = if rejection.is_not_found() {
|
||||
http::StatusCode::NOT_FOUND
|
||||
} else if let Some(body::BodyDeserializeError { .. }) = rejection.find() {
|
||||
http::StatusCode::BAD_REQUEST
|
||||
} else {
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR
|
||||
};
|
||||
http::Response::builder()
|
||||
.status(code)
|
||||
.header("content-type", "application/json")
|
||||
.body(Vec::new())
|
||||
.unwrap()
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpIntegration for TestWarpIntegration {
|
||||
fn get(&self, url: &str) -> TestResponse {
|
||||
use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};
|
||||
|
||||
let url: String = utf8_percent_encode(&url.replace("/?", ""), QUERY_ENCODE_SET)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
self.make_request(
|
||||
warp::test::request()
|
||||
.method("GET")
|
||||
.path(&format!("/?{}", url)),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_json(&self, url: &str, body: &str) -> TestResponse {
|
||||
self.make_request(
|
||||
warp::test::request()
|
||||
.method("POST")
|
||||
.header("content-type", "application/json")
|
||||
.path(url)
|
||||
.body(body),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_graphql(&self, url: &str, body: &str) -> TestResponse {
|
||||
self.make_request(
|
||||
warp::test::request()
|
||||
.method("POST")
|
||||
.header("content-type", "application/graphql")
|
||||
.path(url)
|
||||
.body(body),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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