From 50605ee73e4766f1ed466d742b408ca5e5e303d2 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Fri, 15 Nov 2019 21:50:35 +0100 Subject: [PATCH] Duplicate juniper_rocket for async This is required because rocket only supports futures on a git branch. --- Cargo.toml | 2 + juniper_codegen/src/util.rs | 1 + juniper_rocket/Cargo.toml | 7 +- juniper_rocket/src/lib.rs | 131 +--- juniper_rocket_async/.gitignore | 2 + juniper_rocket_async/CHANGELOG.md | 60 ++ juniper_rocket_async/Cargo.toml | 30 + juniper_rocket_async/LICENSE | 25 + juniper_rocket_async/Makefile.toml | 17 + juniper_rocket_async/README.md | 35 + .../examples/rocket_server.rs | 44 ++ juniper_rocket_async/release.toml | 8 + juniper_rocket_async/src/lib.rs | 658 ++++++++++++++++++ .../tests/custom_response_tests.rs | 11 + 14 files changed, 928 insertions(+), 103 deletions(-) create mode 100644 juniper_rocket_async/.gitignore create mode 100644 juniper_rocket_async/CHANGELOG.md create mode 100644 juniper_rocket_async/Cargo.toml create mode 100644 juniper_rocket_async/LICENSE create mode 100644 juniper_rocket_async/Makefile.toml create mode 100644 juniper_rocket_async/README.md create mode 100644 juniper_rocket_async/examples/rocket_server.rs create mode 100644 juniper_rocket_async/release.toml create mode 100644 juniper_rocket_async/src/lib.rs create mode 100644 juniper_rocket_async/tests/custom_response_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 8a2553e3..af808a8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,6 @@ members = [ exclude = [ "docs/book/tests", "examples/warp_async", + # TODO enable async tests + "juniper_rocket_async", ] diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util.rs index 172ee33e..585e791f 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util.rs @@ -666,6 +666,7 @@ pub struct GraphQLTypeDefiniton { } impl GraphQLTypeDefiniton { + #[allow(unused)] fn has_async_field(&self) -> bool { self.fields.iter().any(|field| field.is_async) } diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index eba95bf7..cd77aad6 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -11,18 +11,13 @@ documentation = "https://docs.rs/juniper_rocket" repository = "https://github.com/graphql-rust/juniper" edition = "2018" -[features] -async = [ "juniper/async" ] - [dependencies] serde = { version = "1.0.2" } serde_json = { version = "1.0.2" } serde_derive = { version = "1.0.2" } juniper = { version = "0.14.1", default-features = false, path = "../juniper"} -futures = { version = "=0.3.1", features = ["compat"] } -rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "async" } -tokio = "=0.2.0-alpha.6" +rocket = { version = "0.4.0" } [dev-dependencies.juniper] version = "0.14.1" diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index face9b74..a016f132 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -39,13 +39,16 @@ Check the LICENSE file for details. #![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")] #![feature(decl_macro, proc_macro_hygiene)] -use std::{error::Error, io::Cursor}; +use std::{ + error::Error, + io::{Cursor, Read}, +}; use rocket::{ - data::{FromDataFuture, FromDataSimple}, + data::{FromDataSimple, Outcome as FromDataOutcome}, http::{ContentType, RawStr, Status}, request::{FormItems, FromForm, FromFormValue}, - response::{content, Responder, Response, ResultFuture}, + response::{content, Responder, Response}, Data, Outcome::{Failure, Forward, Success}, Request, @@ -54,22 +57,16 @@ use rocket::{ use juniper::{http, InputValue}; use juniper::{ - serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode, + serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode, ScalarRefValue, ScalarValue, }; -#[cfg(feature = "async")] -use juniper::GraphQLTypeAsync; - -#[cfg(feature = "async")] -use futures::future::{FutureExt, TryFutureExt}; - #[derive(Debug, serde_derive::Deserialize, PartialEq)] #[serde(untagged)] #[serde(bound = "InputValue: Deserialize<'de>")] enum GraphQLBatchRequest where - S: ScalarValue + Sync + Send, + S: ScalarValue, { Single(http::GraphQLRequest), Batch(Vec>), @@ -79,7 +76,7 @@ where #[serde(untagged)] enum GraphQLBatchResponse<'a, S = DefaultScalarValue> where - S: ScalarValue + Sync + Send, + S: ScalarValue, { Single(http::GraphQLResponse<'a, S>), Batch(Vec>), @@ -87,7 +84,8 @@ where impl GraphQLBatchRequest where - S: ScalarValue + Send + Sync, + S: ScalarValue, + for<'b> &'b S: ScalarRefValue<'b>, { pub fn execute<'a, CtxT, QueryT, MutationT>( &'a self, @@ -111,34 +109,6 @@ where } } - #[cfg(feature = "async")] - pub async fn execute_async<'a, CtxT, QueryT, MutationT>( - &'a self, - root_node: &'a RootNode<'_, QueryT, MutationT, S>, - context: &'a CtxT, - ) -> GraphQLBatchResponse<'a, S> - where - QueryT: GraphQLTypeAsync + Send + Sync, - QueryT::TypeInfo: Send + Sync, - MutationT: GraphQLTypeAsync + Send + Sync, - MutationT::TypeInfo: Send + Sync, - CtxT: Send + Sync, - { - match self { - &GraphQLBatchRequest::Single(ref request) => { - GraphQLBatchResponse::Single(request.execute_async(root_node, context).await) - } - &GraphQLBatchRequest::Batch(ref requests) => { - let futures = requests - .iter() - .map(|request| request.execute_async(root_node, context)) - .collect::>(); - - GraphQLBatchResponse::Batch(futures::future::join_all(futures).await) - } - } - } - pub fn operation_names(&self) -> Vec> { match self { GraphQLBatchRequest::Single(req) => vec![req.operation_name()], @@ -151,7 +121,7 @@ where impl<'a, S> GraphQLBatchResponse<'a, S> where - S: ScalarValue + Send + Sync, + S: ScalarValue, { fn is_ok(&self) -> bool { match self { @@ -171,7 +141,7 @@ where #[derive(Debug, PartialEq)] pub struct GraphQLRequest(GraphQLBatchRequest) where - S: ScalarValue + Send + Sync; + S: ScalarValue; /// Simple wrapper around the result of executing a GraphQL query pub struct GraphQLResponse(pub Status, pub String); @@ -190,7 +160,8 @@ pub fn playground_source(graphql_endpoint_url: &str) -> content::Html { impl GraphQLRequest where - S: ScalarValue + Sync + Send, + S: ScalarValue, + for<'b> &'b S: ScalarRefValue<'b>, { /// Execute an incoming GraphQL query pub fn execute( @@ -213,31 +184,6 @@ where GraphQLResponse(status, json) } - /// Asynchronously execute an incoming GraphQL query - #[cfg(feature = "async")] - pub async fn execute_async( - &self, - root_node: &RootNode<'_, QueryT, MutationT, S>, - context: &CtxT, - ) -> GraphQLResponse - where - QueryT: GraphQLTypeAsync + Send + Sync, - QueryT::TypeInfo: Send + Sync, - MutationT: GraphQLTypeAsync + Send + Sync, - MutationT::TypeInfo: Send + Sync, - CtxT: Send + Sync, - { - let response = self.0.execute_async(root_node, context).await; - let status = if response.is_ok() { - Status::Ok - } else { - Status::BadRequest - }; - let json = serde_json::to_string(&response).unwrap(); - - GraphQLResponse(status, json) - } - /// Returns the operation names associated with this request. /// /// For batch requests there will be multiple names. @@ -303,7 +249,7 @@ impl GraphQLResponse { impl<'f, S> FromForm<'f> for GraphQLRequest where - S: ScalarValue + Send + Sync, + S: ScalarValue, { type Error = String; @@ -374,7 +320,7 @@ where impl<'v, S> FromFormValue<'v> for GraphQLRequest where - S: ScalarValue + Send + Sync, + S: ScalarValue, { type Error = String; @@ -385,47 +331,38 @@ where } } -const BODY_LIMIT: u64 = 1024 * 100; - impl FromDataSimple for GraphQLRequest where - S: ScalarValue + Send + Sync, + S: ScalarValue, { type Error = String; - fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> { - use tokio::io::AsyncReadExt as _; - + fn from_data(request: &Request, data: Data) -> FromDataOutcome { if !request.content_type().map_or(false, |ct| ct.is_json()) { - return Box::pin(async move { Forward(data) }); + return Forward(data); } - Box::pin(async move { - let mut body = String::new(); - let mut reader = data.open().take(BODY_LIMIT); - if let Err(e) = reader.read_to_string(&mut body).await { - return Failure((Status::InternalServerError, format!("{:?}", e))); - } + let mut body = String::new(); + if let Err(e) = data.open().read_to_string(&mut body) { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } - match serde_json::from_str(&body) { - Ok(value) => Success(GraphQLRequest(value)), - Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), - } - }) + match serde_json::from_str(&body) { + Ok(value) => Success(GraphQLRequest(value)), + Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))), + } } } impl<'r> Responder<'r> for GraphQLResponse { - fn respond_to(self, _: &Request) -> ResultFuture<'r> { + fn respond_to(self, _: &Request) -> Result, Status> { let GraphQLResponse(status, body) = self; - Box::pin(async move { - Ok(Response::build() - .header(ContentType::new("application", "json")) - .status(status) - .sized_body(Cursor::new(body)) - .finalize()) - }) + Ok(Response::build() + .header(ContentType::new("application", "json")) + .status(status) + .sized_body(Cursor::new(body)) + .finalize()) } } diff --git a/juniper_rocket_async/.gitignore b/juniper_rocket_async/.gitignore new file mode 100644 index 00000000..a9d37c56 --- /dev/null +++ b/juniper_rocket_async/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/juniper_rocket_async/CHANGELOG.md b/juniper_rocket_async/CHANGELOG.md new file mode 100644 index 00000000..01432d4b --- /dev/null +++ b/juniper_rocket_async/CHANGELOG.md @@ -0,0 +1,60 @@ +# master + +- Compatibility with the latest `juniper`. + +# [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1) + +- Compatibility with the latest `juniper`. + +# [[0.5.0] 2019-09-29](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.0) + +- Compatibility with the latest `juniper`. + +# [[0.4.1] 2019-07-29](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.4.1) + +- Compatibility with the latest `juniper`. + +# [[0.4.0] 2019-07-19](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.4.0) + +- Compatibility with the latest `juniper`. + +# [[0.3.0] 2019-05-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.3.0) + +- Expose the operation names from `GraphQLRequest`. +- Compatibility with the latest `juniper`. + +# [0.2.0] 2018-12-17 + +### Rocket updated to v0.4 + +[Rocket](https://rocket.rs) integration now requires Rocket `0.4.0`. This is due +to changes with the way Rocket handles form parsing. Before this update, it was +impossible to leverage Rocket integration with Rocket beyond 0.3.x. + +Check out [Rocket's Changelog](https://github.com/SergioBenitez/Rocket/blob/v0.4/CHANGELOG.md) +for more details on the 0.4 release. + +# juniper_rocket [0.1.3] 2018-09-13 + +- Add `juniper-0.10.0` compatibility. + +# juniper_rocket [0.1.2] 2018-01-13 + +## Changes + +### Rocket updated to `0.3.6` + +[Rocket](https://rocket.rs) integration now requires Rocket `0.3.6` to +support building with recent Rust nightlies. + +Additional information and supported nightly versions can be found in [Rocket's changelog](https://github.com/SergioBenitez/Rocket/blob/master/CHANGELOG.md#version-036-jan-12-2018). + +[#125](https://github.com/graphql-rust/juniper/issues/125) + +### Decoding of query params + +When processing GET requests, query parameters were not properly url_decoded, + +This was fixed by [PR #122](https://github.com/graphql-rust/juniper/pull/128) by @LegNeato. + +This fixed the [issue #116](https://github.com/graphql-rust/juniper/issues/116). diff --git a/juniper_rocket_async/Cargo.toml b/juniper_rocket_async/Cargo.toml new file mode 100644 index 00000000..eba95bf7 --- /dev/null +++ b/juniper_rocket_async/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "juniper_rocket" +version = "0.5.1" +authors = [ + "Magnus Hallin ", + "Christoph Herzog ", +] +description = "Juniper GraphQL integration with Rocket" +license = "BSD-2-Clause" +documentation = "https://docs.rs/juniper_rocket" +repository = "https://github.com/graphql-rust/juniper" +edition = "2018" + +[features] +async = [ "juniper/async" ] + +[dependencies] +serde = { version = "1.0.2" } +serde_json = { version = "1.0.2" } +serde_derive = { version = "1.0.2" } +juniper = { version = "0.14.1", default-features = false, path = "../juniper"} + +futures = { version = "=0.3.1", features = ["compat"] } +rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "async" } +tokio = "=0.2.0-alpha.6" + +[dev-dependencies.juniper] +version = "0.14.1" +features = ["expose-test-schema", "serde_json"] +path = "../juniper" diff --git a/juniper_rocket_async/LICENSE b/juniper_rocket_async/LICENSE new file mode 100644 index 00000000..0ccd1e17 --- /dev/null +++ b/juniper_rocket_async/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2016, Magnus Hallin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/juniper_rocket_async/Makefile.toml b/juniper_rocket_async/Makefile.toml new file mode 100644 index 00000000..8695d6a6 --- /dev/null +++ b/juniper_rocket_async/Makefile.toml @@ -0,0 +1,17 @@ +[tasks.build-verbose] +condition = { channels = ["nightly"] } + +[tasks.build-verbose.windows] +condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } } + +[tasks.test-verbose] +condition = { channels = ["nightly"] } + +[tasks.test-verbose.windows] +condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } } + +[tasks.ci-coverage-flow] +condition = { channels = ["nightly"] } + +[tasks.ci-coverage-flow.windows] +disabled = true \ No newline at end of file diff --git a/juniper_rocket_async/README.md b/juniper_rocket_async/README.md new file mode 100644 index 00000000..9ff6f6e2 --- /dev/null +++ b/juniper_rocket_async/README.md @@ -0,0 +1,35 @@ +# juniper_rocket + +This repository contains the [Rocket][Rocket] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [Api documentation][documentation]. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documetation] +* [Rocket][Iron] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs + + diff --git a/juniper_rocket_async/examples/rocket_server.rs b/juniper_rocket_async/examples/rocket_server.rs new file mode 100644 index 00000000..ec0b0515 --- /dev/null +++ b/juniper_rocket_async/examples/rocket_server.rs @@ -0,0 +1,44 @@ +#![feature(decl_macro, proc_macro_hygiene)] + +use rocket::{response::content, State}; + +use juniper::{ + tests::{model::Database, schema::Query}, + EmptyMutation, RootNode, +}; + +type Schema = RootNode<'static, Query, EmptyMutation>; + +#[rocket::get("/")] +fn graphiql() -> content::Html { + juniper_rocket::graphiql_source("/graphql") +} + +#[rocket::get("/graphql?")] +fn get_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +#[rocket::post("/graphql", data = "")] +fn post_graphql_handler( + context: State, + request: juniper_rocket::GraphQLRequest, + schema: State, +) -> juniper_rocket::GraphQLResponse { + request.execute(&schema, &context) +} + +fn main() { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new(Query, EmptyMutation::::new())) + .mount( + "/", + rocket::routes![graphiql, get_graphql_handler, post_graphql_handler], + ) + .launch(); +} diff --git a/juniper_rocket_async/release.toml b/juniper_rocket_async/release.toml new file mode 100644 index 00000000..0145d187 --- /dev/null +++ b/juniper_rocket_async/release.toml @@ -0,0 +1,8 @@ +no-dev-version = true +pre-release-commit-message = "Release {{crate_name}} {{version}}" +pro-release-commit-message = "Bump {{crate_name}} version to {{next_version}}" +tag-message = "Release {{crate_name}} {{version}}" +upload-doc = false +pre-release-replacements = [ + {file="src/lib.rs", search="docs.rs/juniper_rocket/[a-z0-9\\.-]+", replace="docs.rs/juniper_rocket/{{version}}"}, +] diff --git a/juniper_rocket_async/src/lib.rs b/juniper_rocket_async/src/lib.rs new file mode 100644 index 00000000..face9b74 --- /dev/null +++ b/juniper_rocket_async/src/lib.rs @@ -0,0 +1,658 @@ +/*! + +# juniper_rocket + +This repository contains the [Rocket][Rocket] web server integration for +[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust. + +## Documentation + +For documentation, including guides and examples, check out [Juniper][Juniper]. + +A basic usage example can also be found in the [Api documentation][documentation]. + +## Examples + +Check [examples/rocket_server.rs][example] for example code of a working Rocket +server with GraphQL handlers. + +## Links + +* [Juniper][Juniper] +* [Api Reference][documentation] +* [Rocket][Rocket] + +## License + +This project is under the BSD-2 license. + +Check the LICENSE file for details. + +[Rocket]: https://rocket.rs +[Juniper]: https://github.com/graphql-rust/juniper +[GraphQL]: http://graphql.org +[documentation]: https://docs.rs/juniper_rocket +[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs + +*/ + +#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")] +#![feature(decl_macro, proc_macro_hygiene)] + +use std::{error::Error, io::Cursor}; + +use rocket::{ + data::{FromDataFuture, FromDataSimple}, + http::{ContentType, RawStr, Status}, + request::{FormItems, FromForm, FromFormValue}, + response::{content, Responder, Response, ResultFuture}, + Data, + Outcome::{Failure, Forward, Success}, + Request, +}; + +use juniper::{http, InputValue}; + +use juniper::{ + serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode, + ScalarValue, +}; + +#[cfg(feature = "async")] +use juniper::GraphQLTypeAsync; + +#[cfg(feature = "async")] +use futures::future::{FutureExt, TryFutureExt}; + +#[derive(Debug, serde_derive::Deserialize, PartialEq)] +#[serde(untagged)] +#[serde(bound = "InputValue: Deserialize<'de>")] +enum GraphQLBatchRequest +where + S: ScalarValue + Sync + Send, +{ + Single(http::GraphQLRequest), + Batch(Vec>), +} + +#[derive(serde_derive::Serialize)] +#[serde(untagged)] +enum GraphQLBatchResponse<'a, S = DefaultScalarValue> +where + S: ScalarValue + Sync + Send, +{ + Single(http::GraphQLResponse<'a, S>), + Batch(Vec>), +} + +impl GraphQLBatchRequest +where + S: ScalarValue + Send + Sync, +{ + pub fn execute<'a, CtxT, QueryT, MutationT>( + &'a self, + root_node: &'a RootNode, + context: &CtxT, + ) -> GraphQLBatchResponse<'a, S> + where + QueryT: GraphQLType, + MutationT: GraphQLType, + { + 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(), + ), + } + } + + #[cfg(feature = "async")] + pub async fn execute_async<'a, CtxT, QueryT, MutationT>( + &'a self, + root_node: &'a RootNode<'_, QueryT, MutationT, S>, + context: &'a CtxT, + ) -> GraphQLBatchResponse<'a, S> + where + QueryT: GraphQLTypeAsync + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + CtxT: Send + Sync, + { + match self { + &GraphQLBatchRequest::Single(ref request) => { + GraphQLBatchResponse::Single(request.execute_async(root_node, context).await) + } + &GraphQLBatchRequest::Batch(ref requests) => { + let futures = requests + .iter() + .map(|request| request.execute_async(root_node, context)) + .collect::>(); + + GraphQLBatchResponse::Batch(futures::future::join_all(futures).await) + } + } + } + + pub fn operation_names(&self) -> Vec> { + match self { + GraphQLBatchRequest::Single(req) => vec![req.operation_name()], + GraphQLBatchRequest::Batch(reqs) => { + reqs.iter().map(|req| req.operation_name()).collect() + } + } + } +} + +impl<'a, S> GraphQLBatchResponse<'a, S> +where + S: ScalarValue + Send + Sync, +{ + 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(GraphQLBatchRequest) +where + S: ScalarValue + Send + Sync; + +/// Simple wrapper around the result of executing a GraphQL query +pub struct GraphQLResponse(pub Status, pub String); + +/// Generate an HTML page containing GraphiQL +pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html { + content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url)) +} + +/// Generate an HTML page containing GraphQL Playground +pub fn playground_source(graphql_endpoint_url: &str) -> content::Html { + content::Html(juniper::http::playground::playground_source( + graphql_endpoint_url, + )) +} + +impl GraphQLRequest +where + S: ScalarValue + Sync + Send, +{ + /// Execute an incoming GraphQL query + pub fn execute( + &self, + root_node: &RootNode, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + { + let response = self.0.execute(root_node, context); + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } + + /// Asynchronously execute an incoming GraphQL query + #[cfg(feature = "async")] + pub async fn execute_async( + &self, + root_node: &RootNode<'_, QueryT, MutationT, S>, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLTypeAsync + Send + Sync, + QueryT::TypeInfo: Send + Sync, + MutationT: GraphQLTypeAsync + Send + Sync, + MutationT::TypeInfo: Send + Sync, + CtxT: Send + Sync, + { + let response = self.0.execute_async(root_node, context).await; + let status = if response.is_ok() { + Status::Ok + } else { + Status::BadRequest + }; + let json = serde_json::to_string(&response).unwrap(); + + GraphQLResponse(status, json) + } + + /// Returns the operation names associated with this request. + /// + /// For batch requests there will be multiple names. + pub fn operation_names(&self) -> Vec> { + self.0.operation_names() + } +} + +impl GraphQLResponse { + /// Constructs an error response outside of the normal execution flow + /// + /// # Examples + /// + /// ``` + /// # #![feature(decl_macro, proc_macro_hygiene)] + /// # + /// # extern crate juniper; + /// # extern crate juniper_rocket; + /// # extern crate rocket; + /// # + /// # use rocket::http::Cookies; + /// # use rocket::request::Form; + /// # use rocket::response::content; + /// # use rocket::State; + /// # + /// # use juniper::tests::schema::Query; + /// # use juniper::tests::model::Database; + /// # use juniper::{EmptyMutation, FieldError, RootNode, Value}; + /// # + /// # type Schema = RootNode<'static, Query, EmptyMutation>; + /// # + /// #[rocket::get("/graphql?")] + /// fn get_graphql_handler( + /// mut cookies: Cookies, + /// context: State, + /// request: Form, + /// schema: State, + /// ) -> juniper_rocket::GraphQLResponse { + /// if cookies.get_private("user_id").is_none() { + /// let err = FieldError::new("User is not logged in", Value::null()); + /// return juniper_rocket::GraphQLResponse::error(err); + /// } + /// + /// request.execute(&schema, &context) + /// } + /// ``` + pub fn error(error: FieldError) -> Self { + let response = http::GraphQLResponse::error(error); + let json = serde_json::to_string(&response).unwrap(); + GraphQLResponse(Status::BadRequest, json) + } + + /// Constructs a custom response outside of the normal execution flow + /// + /// This is intended for highly customized integrations and should only + /// be used as a last resort. For normal juniper use, use the response + /// from GraphQLRequest::execute(..). + pub fn custom(status: Status, response: serde_json::Value) -> Self { + let json = serde_json::to_string(&response).unwrap(); + GraphQLResponse(status, json) + } +} + +impl<'f, S> FromForm<'f> for GraphQLRequest +where + S: ScalarValue + Send + Sync, +{ + type Error = String; + + fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result { + let mut query = None; + let mut operation_name = None; + let mut variables = None; + + for form_item in form_items { + let (key, value) = form_item.key_value(); + // Note: we explicitly decode in the match arms to save work rather + // than decoding every form item blindly. + match key.as_str() { + "query" => { + if query.is_some() { + return Err("Query parameter must not occur more than once".to_owned()); + } else { + match value.url_decode() { + Ok(v) => query = Some(v), + Err(e) => return Err(e.description().to_string()), + } + } + } + "operation_name" => { + if operation_name.is_some() { + return Err( + "Operation name parameter must not occur more than once".to_owned() + ); + } else { + match value.url_decode() { + Ok(v) => operation_name = Some(v), + Err(e) => return Err(e.description().to_string()), + } + } + } + "variables" => { + if variables.is_some() { + return Err("Variables parameter must not occur more than once".to_owned()); + } else { + let decoded; + match value.url_decode() { + Ok(v) => decoded = v, + Err(e) => return Err(e.description().to_string()), + } + variables = Some( + serde_json::from_str::>(&decoded) + .map_err(|err| err.description().to_owned())?, + ); + } + } + _ => { + if strict { + return Err(format!("Prohibited extra field '{}'", key).to_owned()); + } + } + } + } + + if let Some(query) = query { + Ok(GraphQLRequest(GraphQLBatchRequest::Single( + http::GraphQLRequest::new(query, operation_name, variables), + ))) + } else { + Err("Query parameter missing".to_owned()) + } + } +} + +impl<'v, S> FromFormValue<'v> for GraphQLRequest +where + S: ScalarValue + Send + Sync, +{ + type Error = String; + + fn from_form_value(form_value: &'v RawStr) -> Result { + let mut form_items = FormItems::from(form_value); + + Self::from_form(&mut form_items, true) + } +} + +const BODY_LIMIT: u64 = 1024 * 100; + +impl FromDataSimple for GraphQLRequest +where + S: ScalarValue + Send + Sync, +{ + type Error = String; + + fn from_data(request: &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) }); + } + + Box::pin(async move { + let mut body = String::new(); + let mut reader = data.open().take(BODY_LIMIT); + if let Err(e) = reader.read_to_string(&mut body).await { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } + + match serde_json::from_str(&body) { + Ok(value) => Success(GraphQLRequest(value)), + Err(failure) => Failure((Status::BadRequest, format!("{}", failure))), + } + }) + } +} + +impl<'r> Responder<'r> for GraphQLResponse { + fn respond_to(self, _: &Request) -> ResultFuture<'r> { + let GraphQLResponse(status, body) = self; + + Box::pin(async move { + Ok(Response::build() + .header(ContentType::new("application", "json")) + .status(status) + .sized_body(Cursor::new(body)) + .finalize()) + }) + } +} + +#[cfg(test)] +mod fromform_tests { + use super::*; + use juniper::InputValue; + use rocket::request::{FormItems, FromForm}; + use std::str; + + fn check_error(input: &str, error: &str, strict: bool) { + let mut items = FormItems::from(input); + let result: Result = GraphQLRequest::from_form(&mut items, strict); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), error); + } + + #[test] + fn test_empty_form() { + check_error("", "Query parameter missing", false); + } + + #[test] + fn test_no_query() { + check_error( + "operation_name=foo&variables={}", + "Query parameter missing", + false, + ); + } + + #[test] + fn test_strict() { + check_error("query=test&foo=bar", "Prohibited extra field \'foo\'", true); + } + + #[test] + fn test_duplicate_query() { + check_error( + "query=foo&query=bar", + "Query parameter must not occur more than once", + false, + ); + } + + #[test] + fn test_duplicate_operation_name() { + check_error( + "query=test&operation_name=op1&operation_name=op2", + "Operation name parameter must not occur more than once", + false, + ); + } + + #[test] + fn test_duplicate_variables() { + check_error( + "query=test&variables={}&variables={}", + "Variables parameter must not occur more than once", + false, + ); + } + + #[test] + fn test_variables_invalid_json() { + check_error("query=test&variables=NOT_JSON", "JSON error", false); + } + + #[test] + fn test_variables_valid_json() { + let form_string = r#"query=test&variables={"foo":"bar"}"#; + let mut items = FormItems::from(form_string); + let result = GraphQLRequest::from_form(&mut items, false); + assert!(result.is_ok()); + let variables = ::serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap(); + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + "test".to_string(), + None, + Some(variables), + ))); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_variables_encoded_json() { + let form_string = r#"query=test&variables={"foo": "x%20y%26%3F+z"}"#; + let mut items = FormItems::from(form_string); + let result = GraphQLRequest::from_form(&mut items, false); + assert!(result.is_ok()); + let variables = ::serde_json::from_str::(r#"{"foo":"x y&? z"}"#).unwrap(); + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + "test".to_string(), + None, + Some(variables), + ))); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_url_decode() { + let form_string = "query=%25foo%20bar+baz%26%3F&operation_name=test"; + let mut items = FormItems::from(form_string); + let result: Result = GraphQLRequest::from_form(&mut items, false); + assert!(result.is_ok()); + let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( + "%foo bar baz&?".to_string(), + Some("test".to_string()), + None, + ))); + assert_eq!(result.unwrap(), expected); + } +} + +#[cfg(test)] +mod tests { + + use rocket::{ + self, get, + http::ContentType, + local::{Client, LocalRequest}, + post, + request::Form, + routes, Rocket, State, + }; + + use juniper::{ + http::tests as http_tests, + tests::{model::Database, schema::Query}, + EmptyMutation, RootNode, + }; + + type Schema = RootNode<'static, Query, EmptyMutation>; + + #[get("/?")] + fn get_graphql_handler( + context: State, + request: Form, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + #[post("/", data = "")] + fn post_graphql_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + request.execute(&schema, &context) + } + + struct TestRocketIntegration { + client: Client, + } + + 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 { + let req = &self.client.post(url).header(ContentType::JSON).body(body); + make_test_response(req) + } + } + + #[test] + fn test_rocket_integration() { + let rocket = make_rocket(); + let client = Client::new(rocket).expect("valid rocket"); + let integration = TestRocketIntegration { client }; + + http_tests::run_http_test_suite(&integration); + } + + #[test] + fn test_operation_names() { + #[post("/", data = "")] + fn post_graphql_assert_operation_name_handler( + context: State, + request: super::GraphQLRequest, + schema: State, + ) -> super::GraphQLResponse { + assert_eq!(request.operation_names(), vec![Some("TestQuery")]); + request.execute(&schema, &context) + } + + let rocket = make_rocket_without_routes() + .mount("/", routes![post_graphql_assert_operation_name_handler]); + let client = Client::new(rocket).expect("valid rocket"); + + let req = client + .post("/") + .header(ContentType::JSON) + .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#); + let resp = make_test_response(&req); + + assert_eq!(resp.status_code, 200); + } + + fn make_rocket() -> Rocket { + make_rocket_without_routes().mount("/", routes![post_graphql_handler, get_graphql_handler]) + } + + fn make_rocket_without_routes() -> Rocket { + rocket::ignite() + .manage(Database::new()) + .manage(Schema::new(Query, EmptyMutation::::new())) + } + + fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse { + let mut response = request.clone().dispatch(); + let status_code = response.status().code as i32; + let content_type = response + .content_type() + .expect("No content type header from handler") + .to_string(); + let body = response + .body() + .expect("No body returned from GraphQL handler") + .into_string(); + + http_tests::TestResponse { + status_code, + body, + content_type, + } + } +} diff --git a/juniper_rocket_async/tests/custom_response_tests.rs b/juniper_rocket_async/tests/custom_response_tests.rs new file mode 100644 index 00000000..aeb29d4b --- /dev/null +++ b/juniper_rocket_async/tests/custom_response_tests.rs @@ -0,0 +1,11 @@ +extern crate juniper_rocket; +extern crate rocket; + +use rocket::http::Status; + +use juniper_rocket::GraphQLResponse; + +#[test] +fn test_graphql_response_is_public() { + let _ = GraphQLResponse(Status::Unauthorized, "Unauthorized".to_string()); +}