diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 0176fcd4..be2a1dd0 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -37,7 +37,7 @@ juniper_codegen = { version = "0.9.2", path = "../juniper_codegen" } fnv = "1.0.3" indexmap = { version = "1.0.0", features = ["serde-1"] } serde = { version = "1.0.8" } -serde_derive = {version="1.0.2" } +serde_derive = { version = "1.0.2" } chrono = { version = "0.4.0", optional = true } serde_json = { version="1.0.2", optional = true } diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 96121537..6a32dfa6 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -165,6 +165,9 @@ pub mod tests { println!(" - test_simple_post"); test_simple_post(integration); + + println!(" - test_batched_post"); + test_batched_post(integration); } fn unwrap_json_response(response: &TestResponse) -> Json { @@ -257,4 +260,17 @@ pub mod tests { .expect("Invalid JSON constant in test") ); } + + fn test_batched_post<T: HTTPIntegration>(integration: &T) { + let response = integration.post("/", r#"[{"query": "{hero{name}}"}, {"query": "{hero{name}}"}]"#); + + assert_eq!(response.status_code, 200); + assert_eq!(response.content_type, "application/json"); + + assert_eq!( + unwrap_json_response(&response), + serde_json::from_str::<Json>(r#"[{"data": {"hero": {"name": "R2-D2"}}}, {"data": {"hero": {"name": "R2-D2"}}}]"#) + .expect("Invalid JSON constant in test") + ); + } } diff --git a/juniper_iron/Cargo.toml b/juniper_iron/Cargo.toml index 440df4f2..df540d5d 100644 --- a/juniper_iron/Cargo.toml +++ b/juniper_iron/Cargo.toml @@ -13,6 +13,7 @@ repository = "https://github.com/graphql-rust/juniper" [dependencies] serde = { version = "1.0.2" } serde_json = { version = "1.0.2" } +serde_derive = { version = "1.0.2" } juniper = { version = "0.9.2", path = "../juniper" } urlencoded = { version = ">= 0.5, < 0.7" } diff --git a/juniper_iron/src/lib.rs b/juniper_iron/src/lib.rs index 58395e2e..24cf86ef 100644 --- a/juniper_iron/src/lib.rs +++ b/juniper_iron/src/lib.rs @@ -107,6 +107,8 @@ extern crate iron; extern crate iron_test; extern crate juniper; extern crate serde_json; +#[macro_use] +extern crate serde_derive; extern crate urlencoded; use iron::prelude::*; @@ -125,6 +127,48 @@ use serde_json::error::Error as SerdeError; use juniper::{GraphQLType, InputValue, RootNode}; use juniper::http; +#[derive(Deserialize)] +#[serde(untagged)] +enum GraphQLBatchRequest { + Single(http::GraphQLRequest), + Batch(Vec<http::GraphQLRequest>), +} + +#[derive(Serialize)] +#[serde(untagged)] +enum GraphQLBatchResponse<'a> { + Single(http::GraphQLResponse<'a>), + Batch(Vec<http::GraphQLResponse<'a>>), +} + +impl GraphQLBatchRequest { + pub fn execute<'a, CtxT, QueryT, MutationT>( + &'a self, + root_node: &RootNode<QueryT, MutationT>, + context: &CtxT, + ) -> GraphQLBatchResponse<'a> + where + QueryT: GraphQLType<Context = CtxT>, + MutationT: GraphQLType<Context = CtxT>, + { + match self { + &GraphQLBatchRequest::Single(ref request) => + GraphQLBatchResponse::Single(request.execute(root_node, context)), + &GraphQLBatchRequest::Batch(ref requests) => + GraphQLBatchResponse::Batch(requests.iter().map(|request| request.execute(root_node, context)).collect()), + } + } +} + +impl<'a> GraphQLBatchResponse<'a> { + fn is_ok(&self) -> bool { + match self { + &GraphQLBatchResponse::Single(ref response) => response.is_ok(), + &GraphQLBatchResponse::Batch(ref responses) => responses.iter().fold(true, |ok, response| ok && response.is_ok()), + } + } +} + /// Handler that executes `GraphQL` queries in the given schema /// /// The handler responds to GET requests and POST requests only. In GET @@ -199,7 +243,7 @@ where } } - fn handle_get(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> { + fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest> { let url_query_string = req.get_mut::<UrlEncodedQuery>() .map_err(GraphQLIronError::Url)?; @@ -208,24 +252,24 @@ where let operation_name = parse_url_param(url_query_string.remove("operationName"))?; let variables = parse_variable_param(url_query_string.remove("variables"))?; - Ok(http::GraphQLRequest::new( + Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new( input_query, operation_name, variables, - )) + ))) } - fn handle_post(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> { + fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest> { let mut request_payload = String::new(); itry!(req.body.read_to_string(&mut request_payload)); Ok( - serde_json::from_str::<http::GraphQLRequest>(request_payload.as_str()) + serde_json::from_str::<GraphQLBatchRequest>(request_payload.as_str()) .map_err(GraphQLIronError::Serde)?, ) } - fn execute(&self, context: &CtxT, request: http::GraphQLRequest) -> IronResult<Response> { + fn execute(&self, context: &CtxT, request: GraphQLBatchRequest) -> IronResult<Response> { let response = request.execute(&self.root_node, context); let content_type = "application/json".parse::<Mime>().unwrap(); let json = serde_json::to_string_pretty(&response).unwrap(); diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index bc123b04..547e487b 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -12,8 +12,8 @@ repository = "https://github.com/graphql-rust/juniper" [dependencies] serde = { version = "1.0.2" } -serde_derive = {version="1.0.2" } serde_json = { version = "1.0.2" } +serde_derive = { version = "1.0.2" } juniper = { version = "0.9.2" , path = "../juniper"} rocket = { version = "0.3.9" } diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index 70a82390..a204d212 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -42,6 +42,8 @@ Check the LICENSE file for details. extern crate juniper; extern crate rocket; extern crate serde_json; +#[macro_use] +extern crate serde_derive; use std::io::{Cursor, Read}; use std::error::Error; @@ -61,13 +63,55 @@ use juniper::GraphQLType; use juniper::FieldError; use juniper::RootNode; +#[derive(Debug, Deserialize, PartialEq)] +#[serde(untagged)] +enum GraphQLBatchRequest { + Single(http::GraphQLRequest), + Batch(Vec<http::GraphQLRequest>), +} + +#[derive(Serialize)] +#[serde(untagged)] +enum GraphQLBatchResponse<'a> { + Single(http::GraphQLResponse<'a>), + Batch(Vec<http::GraphQLResponse<'a>>), +} + +impl GraphQLBatchRequest { + pub fn execute<'a, CtxT, QueryT, MutationT>( + &'a self, + root_node: &RootNode<QueryT, MutationT>, + context: &CtxT, + ) -> GraphQLBatchResponse<'a> + where + QueryT: GraphQLType<Context = CtxT>, + MutationT: GraphQLType<Context = CtxT>, + { + match self { + &GraphQLBatchRequest::Single(ref request) => + GraphQLBatchResponse::Single(request.execute(root_node, context)), + &GraphQLBatchRequest::Batch(ref requests) => + GraphQLBatchResponse::Batch(requests.iter().map(|request| request.execute(root_node, context)).collect()), + } + } +} + +impl<'a> GraphQLBatchResponse<'a> { + fn is_ok(&self) -> bool { + match self { + &GraphQLBatchResponse::Single(ref response) => response.is_ok(), + &GraphQLBatchResponse::Batch(ref responses) => responses.iter().fold(true, |ok, response| ok && response.is_ok()), + } + } +} + /// Simple wrapper around an incoming GraphQL request /// /// See the `http` module for more information. This type can be constructed /// automatically from both GET and POST routes by implementing the `FromForm` /// and `FromData` traits. #[derive(Debug, PartialEq)] -pub struct GraphQLRequest(http::GraphQLRequest); +pub struct GraphQLRequest(GraphQLBatchRequest); /// Simple wrapper around the result of executing a GraphQL query pub struct GraphQLResponse(Status, String); @@ -209,11 +253,11 @@ impl<'f> FromForm<'f> for GraphQLRequest { } if let Some(query) = query { - Ok(GraphQLRequest(http::GraphQLRequest::new( + Ok(GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( query, operation_name, variables, - ))) + )))) } else { Err("Query parameter missing".to_owned()) } @@ -324,11 +368,11 @@ mod fromform_tests { let result = GraphQLRequest::from_form(&mut items, false); assert!(result.is_ok()); let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"bar"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLRequest::new( + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( "test".to_string(), None, Some(variables), - )); + ))); assert_eq!(result.unwrap(), expected); } @@ -339,11 +383,11 @@ mod fromform_tests { let result = GraphQLRequest::from_form(&mut items, false); assert!(result.is_ok()); let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"x y&? z"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLRequest::new( + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( "test".to_string(), None, Some(variables), - )); + ))); assert_eq!(result.unwrap(), expected); } @@ -353,11 +397,11 @@ mod fromform_tests { let mut items = FormItems::from(form_string); let result = GraphQLRequest::from_form(&mut items, false); assert!(result.is_ok()); - let expected = GraphQLRequest(http::GraphQLRequest::new( + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( "%foo bar baz&?".to_string(), Some("test".to_string()), None, - )); + ))); assert_eq!(result.unwrap(), expected); } }