From 93deb2862a5c19616ca556db0cfe961af0093977 Mon Sep 17 00:00:00 2001 From: Christian Legnitto <LegNeato@users.noreply.github.com> Date: Wed, 7 Jul 2021 06:26:22 -1000 Subject: [PATCH] Make juniper_rocket_async replace juniper_rocket (#955) * Make juniper_rocket_async replace juniper_rocket Now that rocket v0.5 is async and in rc, merge the two juniper projects. Fixes https://github.com/graphql-rust/juniper/issues/947. * Remove println --- Cargo.toml | 1 - Makefile.toml | 8 +- juniper/release.toml | 3 - juniper_rocket/CHANGELOG.md | 1 + juniper_rocket/Cargo.toml | 5 +- juniper_rocket/Makefile.toml | 17 - juniper_rocket/examples/rocket_server.rs | 26 +- juniper_rocket/src/lib.rs | 461 ++++++++----- juniper_rocket/tests/custom_response_tests.rs | 3 +- juniper_rocket_async/.gitignore | 2 - juniper_rocket_async/CHANGELOG.md | 62 -- juniper_rocket_async/Cargo.toml | 21 - juniper_rocket_async/LICENSE | 25 - juniper_rocket_async/README.md | 35 - .../examples/rocket_server.rs | 48 -- juniper_rocket_async/src/lib.rs | 643 ------------------ .../tests/custom_response_tests.rs | 7 - 17 files changed, 303 insertions(+), 1065 deletions(-) delete mode 100644 juniper_rocket/Makefile.toml delete mode 100644 juniper_rocket_async/.gitignore delete mode 100644 juniper_rocket_async/CHANGELOG.md delete mode 100644 juniper_rocket_async/Cargo.toml delete mode 100644 juniper_rocket_async/LICENSE delete mode 100644 juniper_rocket_async/README.md delete mode 100644 juniper_rocket_async/examples/rocket_server.rs delete mode 100644 juniper_rocket_async/src/lib.rs delete mode 100644 juniper_rocket_async/tests/custom_response_tests.rs diff --git a/Cargo.toml b/Cargo.toml index ddb54b5b..a64f814f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ members = [ "juniper_hyper", "juniper_iron", "juniper_rocket", - "juniper_rocket_async", "juniper_subscriptions", "juniper_graphql_ws", "juniper_warp", diff --git a/Makefile.toml b/Makefile.toml index 36d6e955..f6c96e19 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,7 +12,7 @@ CARGO_MAKE_CARGO_ALL_FEATURES = "" [tasks.release] condition = { env_set = [ "RELEASE_LEVEL" ] } workspace = false -env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" } +env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" } run_task = { name = "release-INTERNAL", fork = true } @@ -39,7 +39,7 @@ args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/releas [tasks.release-dry-run] condition = { env_set = [ "RELEASE_LEVEL" ] } workspace = false -env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" } +env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" } run_task = { name = "release-dry-run-INTERNAL", fork = true } [tasks.release-some-dry-run] @@ -53,7 +53,7 @@ run_task = { name = "release-some-dry-run-INTERNAL", fork = true } [tasks.release-dry-run-INTERNAL] private = true condition = { env_set = [ "RELEASE_LEVEL" ] } -env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" } +env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" } description = "Run `cargo-release --dry-run` for every crate" command = "cargo-release" args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/release.toml", "--dry-run", "${RELEASE_LEVEL}"] @@ -68,7 +68,7 @@ args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/releas [tasks.release-local-test] condition = { env_set = [ "RELEASE_LEVEL" ] } workspace = false -env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" } +env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" } run_task = { name = "release-local-test-INTERNAL", fork = true } diff --git a/juniper/release.toml b/juniper/release.toml index 8779c9b4..7831d4e2 100644 --- a/juniper/release.toml +++ b/juniper/release.toml @@ -21,9 +21,6 @@ pre-release-replacements = [ # Rocket {file="../juniper_rocket/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""}, {file="../juniper_rocket/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""}, - # Rocket Async - {file="../juniper_rocket_async/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""}, - {file="../juniper_rocket_async/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""}, # Warp {file="../juniper_warp/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""}, {file="../juniper_warp/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""}, diff --git a/juniper_rocket/CHANGELOG.md b/juniper_rocket/CHANGELOG.md index fdc8e016..e9005978 100644 --- a/juniper_rocket/CHANGELOG.md +++ b/juniper_rocket/CHANGELOG.md @@ -1,5 +1,6 @@ # master +- Require async rocket support (`rocket` >= 0.5-rc1). - Compatibility with the latest `juniper`. # [[0.7.1] 2021-06-07](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.7.1) diff --git a/juniper_rocket/Cargo.toml b/juniper_rocket/Cargo.toml index a8da2c45..41af3490 100644 --- a/juniper_rocket/Cargo.toml +++ b/juniper_rocket/Cargo.toml @@ -12,8 +12,9 @@ documentation = "https://docs.rs/juniper_rocket" repository = "https://github.com/graphql-rust/juniper" [dependencies] -juniper = { version = "0.15.6", path = "../juniper", default-features = false} -rocket = { version = "0.4.10", default-features = false } +futures = "0.3.1" +juniper = { version = "0.15.6", path = "../juniper", default-features = false } +rocket = { version = "0.5.0-rc.1", default-features = false } serde_json = "1.0.2" [dev-dependencies] diff --git a/juniper_rocket/Makefile.toml b/juniper_rocket/Makefile.toml deleted file mode 100644 index 8695d6a6..00000000 --- a/juniper_rocket/Makefile.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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/examples/rocket_server.rs b/juniper_rocket/examples/rocket_server.rs index 0a55adad..76a6b296 100644 --- a/juniper_rocket/examples/rocket_server.rs +++ b/juniper_rocket/examples/rocket_server.rs @@ -1,11 +1,8 @@ -#![feature(decl_macro, proc_macro_hygiene)] - -use rocket::{response::content, State}; - use juniper::{ tests::fixtures::starwars::schema::{Database, Query}, EmptyMutation, EmptySubscription, RootNode, }; +use rocket::{response::content, Rocket, State}; type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; @@ -16,24 +13,25 @@ fn graphiql() -> content::Html<String> { #[rocket::get("/graphql?<request>")] fn get_graphql_handler( - context: State<Database>, + context: &State<Database>, request: juniper_rocket::GraphQLRequest, - schema: State<Schema>, + schema: &State<Schema>, ) -> juniper_rocket::GraphQLResponse { - request.execute_sync(&schema, &context) + request.execute_sync(&*schema, &*context) } #[rocket::post("/graphql", data = "<request>")] fn post_graphql_handler( - context: State<Database>, + context: &State<Database>, request: juniper_rocket::GraphQLRequest, - schema: State<Schema>, + schema: &State<Schema>, ) -> juniper_rocket::GraphQLResponse { - request.execute_sync(&schema, &context) + request.execute_sync(&*schema, &*context) } -fn main() { - rocket::ignite() +#[rocket::main] +async fn main() { + Rocket::build() .manage(Database::new()) .manage(Schema::new( Query, @@ -44,5 +42,7 @@ fn main() { "/", rocket::routes![graphiql, get_graphql_handler, post_graphql_handler], ) - .launch(); + .launch() + .await + .expect("server to launch"); } diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index e623480c..2ab7898d 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -36,23 +36,23 @@ Check the LICENSE file for details. */ -#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")] -#![feature(decl_macro, proc_macro_hygiene)] +#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.7.1")] -use std::io::{Cursor, Read}; +use std::{borrow::Cow, io::Cursor}; + +use rocket::{ + data::{self, FromData, ToByteUnit}, + form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField}, + http::{ContentType, Status}, + outcome::Outcome::{Failure, Forward, Success}, + response::{self, content, Responder, Response}, + Data, Request, +}; use juniper::{ http::{self, GraphQLBatchRequest}, - DefaultScalarValue, FieldError, GraphQLType, InputValue, RootNode, ScalarValue, -}; -use rocket::{ - data::{FromDataSimple, Outcome as FromDataOutcome}, - http::{ContentType, RawStr, Status}, - request::{FormItems, FromForm, FromFormValue}, - response::{content, Responder, Response}, - Data, - Outcome::{Forward, Success}, - Request, + DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, + InputValue, RootNode, ScalarValue, }; /// Simple wrapper around an incoming GraphQL request @@ -71,22 +71,22 @@ pub struct GraphQLResponse(pub Status, pub String); /// Generate an HTML page containing GraphiQL pub fn graphiql_source( graphql_endpoint_url: &str, - subscriptions_endpoint: Option<&str>, + subscriptions_endpoint_url: Option<&str>, ) -> content::Html<String> { content::Html(juniper::http::graphiql::graphiql_source( graphql_endpoint_url, - subscriptions_endpoint, + subscriptions_endpoint_url, )) } /// Generate an HTML page containing GraphQL Playground pub fn playground_source( graphql_endpoint_url: &str, - subscriptions_endpoint: Option<&str>, + subscriptions_endpoint_url: Option<&str>, ) -> content::Html<String> { content::Html(juniper::http::playground::playground_source( graphql_endpoint_url, - subscriptions_endpoint, + subscriptions_endpoint_url, )) } @@ -94,7 +94,7 @@ impl<S> GraphQLRequest<S> where S: ScalarValue, { - /// Execute an incoming GraphQL query + /// Synchronously execute an incoming GraphQL query. pub fn execute_sync<CtxT, QueryT, MutationT, SubscriptionT>( &self, root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>, @@ -116,6 +116,33 @@ where GraphQLResponse(status, json) } + /// Asynchronously execute an incoming GraphQL query. + pub async fn execute<CtxT, QueryT, MutationT, SubscriptionT>( + &self, + root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>, + context: &CtxT, + ) -> GraphQLResponse + where + QueryT: GraphQLTypeAsync<S, Context = CtxT>, + QueryT::TypeInfo: Sync, + MutationT: GraphQLTypeAsync<S, Context = CtxT>, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT>, + SubscriptionT::TypeInfo: Sync, + CtxT: Sync, + S: Send + Sync, + { + let response = self.0.execute(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. @@ -130,10 +157,8 @@ impl GraphQLResponse { /// # Examples /// /// ``` - /// # #![feature(decl_macro, proc_macro_hygiene)] - /// # - /// # use rocket::http::Cookies; - /// # use rocket::request::Form; + /// # use rocket::http::CookieJar; + /// # use rocket::form::Form; /// # use rocket::response::content; /// # use rocket::State; /// # @@ -144,17 +169,17 @@ impl GraphQLResponse { /// # /// #[rocket::get("/graphql?<request..>")] /// fn get_graphql_handler( - /// mut cookies: Cookies, - /// context: State<Database>, - /// request: Form<juniper_rocket::GraphQLRequest>, - /// schema: State<Schema>, + /// cookies: &CookieJar, + /// context: &State<Database>, + /// request: juniper_rocket::GraphQLRequest, + /// schema: &State<Schema>, /// ) -> juniper_rocket::GraphQLResponse { /// if cookies.get("user_id").is_none() { /// let err = FieldError::new("User is not logged in", Value::null()); /// return juniper_rocket::GraphQLResponse::error(err); /// } /// - /// request.execute_sync(&schema, &context) + /// request.execute_sync(&*schema, &*context) /// } /// ``` pub fn error(error: FieldError) -> Self { @@ -174,128 +199,172 @@ impl GraphQLResponse { } } -impl<'f, S> FromForm<'f> for GraphQLRequest<S> -where - S: ScalarValue, -{ - type Error = String; +pub struct GraphQLContext<'f, S: ScalarValue> { + opts: Options, + query: Option<String>, + operation_name: Option<String>, + variables: Option<InputValue<S>>, + errors: Errors<'f>, +} - fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result<Self, String> { - let mut query = None; - let mut operation_name = None; - let mut variables = None; +impl<'f, S: ScalarValue> GraphQLContext<'f, S> { + fn query(&mut self, value: String) { + if self.query.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("query"); - 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.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.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.to_string()), - } - variables = Some( - serde_json::from_str::<InputValue<_>>(&decoded) - .map_err(|err| err.to_string())?, - ); - } - } - _ => { - if strict { - return Err(format!("Prohibited extra field '{}'", key)); - } + self.errors.push(error) + } else { + self.query = Some(value); + } + } + + fn operation_name(&mut self, value: String) { + if self.operation_name.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("operation_name"); + + self.errors.push(error) + } else { + self.operation_name = Some(value); + } + } + + fn variables(&mut self, value: String) { + if self.variables.is_some() { + let error = Error::from(ErrorKind::Duplicate).with_name("variables"); + + self.errors.push(error) + } else { + let parse_result = serde_json::from_str::<InputValue<S>>(&value); + + match parse_result { + Ok(variables) => self.variables = Some(variables), + Err(e) => { + let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string()))) + .with_name("variables"); + + self.errors.push(error); } } } + } +} - if let Some(query) = query { - Ok(GraphQLRequest(GraphQLBatchRequest::Single( - http::GraphQLRequest::new(query, operation_name, variables), - ))) - } else { - Err("Query parameter missing".to_owned()) +#[rocket::async_trait] +impl<'f, S> FromForm<'f> for GraphQLRequest<S> +where + S: ScalarValue + Send, +{ + type Context = GraphQLContext<'f, S>; + + fn init(opts: Options) -> Self::Context { + GraphQLContext { + opts, + query: None, + operation_name: None, + variables: None, + errors: Errors::new(), + } + } + + fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) { + match field.name.key().map(|key| key.as_str()) { + Some("query") => ctx.query(field.value.to_owned()), + Some("operation_name") => ctx.operation_name(field.value.to_owned()), + Some("variables") => ctx.variables(field.value.to_owned()), + Some(key) => { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unknown).with_name(key); + + ctx.errors.push(error) + } + } + None => { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unexpected); + + ctx.errors.push(error) + } + } + } + } + + async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) { + if ctx.opts.strict { + let error = Error::from(ErrorKind::Unexpected).with_name(field.name); + + ctx.errors.push(error) + } + } + + fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> { + if ctx.query.is_none() { + let error = Error::from(ErrorKind::Missing).with_name("query"); + + ctx.errors.push(error) + } + + match ctx.errors.is_empty() { + true => Ok(GraphQLRequest(GraphQLBatchRequest::Single( + http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), + ))), + false => Err(ctx.errors), } } } -impl<'v, S> FromFormValue<'v> for GraphQLRequest<S> +const BODY_LIMIT: u64 = 1024 * 100; + +#[rocket::async_trait] +impl<'r, S> FromData<'r> for GraphQLRequest<S> where S: ScalarValue, { type Error = String; - fn from_form_value(form_value: &'v RawStr) -> Result<Self, Self::Error> { - let mut form_items = FormItems::from(form_value); + async fn from_data( + req: &'r Request<'_>, + data: Data<'r>, + ) -> data::Outcome<'r, Self, Self::Error> { + use rocket::tokio::io::AsyncReadExt as _; - Self::from_form(&mut form_items, true) - } -} - -impl<S> FromDataSimple for GraphQLRequest<S> -where - S: ScalarValue, -{ - type Error = String; - - 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), + _ => return Box::pin(async move { Forward(data) }).await, }; - let mut body = String::new(); - data.open() - .read_to_string(&mut body) - .map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?; + Box::pin(async move { + let mut body = String::new(); + let mut reader = data.open(BODY_LIMIT.bytes()); + if let Err(e) = reader.read_to_string(&mut body).await { + return Failure((Status::InternalServerError, format!("{:?}", e))); + } - Success(GraphQLRequest(if is_json { - serde_json::from_str(&body).map_err(|e| (Status::BadRequest, format!("{}", e)))? - } else { - GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) - })) + Success(GraphQLRequest(if is_json { + match serde_json::from_str(&body) { + Ok(req) => req, + Err(e) => return Failure((Status::BadRequest, format!("{}", e))), + } + } else { + GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + })) + }) + .await } } -impl<'r> Responder<'r> for GraphQLResponse { - fn respond_to(self, _: &Request) -> Result<Response<'r>, Status> { +impl<'r, 'o: 'r> Responder<'r, 'o> for GraphQLResponse { + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> { let GraphQLResponse(status, body) = self; - Ok(Response::build() + Response::build() .header(ContentType::new("application", "json")) .status(status) - .sized_body(Cursor::new(body)) - .finalize()) + .sized_body(body.len(), Cursor::new(body)) + .ok() } } @@ -303,40 +372,66 @@ impl<'r> Responder<'r> for GraphQLResponse { mod fromform_tests { use super::*; use juniper::InputValue; - use rocket::request::{FormItems, FromForm}; - use std::str; + use rocket::{ + form::{error::ErrorKind, Error, Form, Strict}, + http::RawStr, + }; + use std::borrow::Cow; - fn check_error(input: &str, error: &str, strict: bool) { - let mut items = FormItems::from(input); - let result: Result<GraphQLRequest, _> = GraphQLRequest::from_form(&mut items, strict); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), error); + fn check_error(input: &str, expected_errors: Vec<Error>, strict: bool) { + let errors = if strict { + let result = Form::<Strict<GraphQLRequest>>::parse_encoded(RawStr::new(input)); + assert!(result.is_err()); + result.unwrap_err() + } else { + let result = Form::<GraphQLRequest>::parse_encoded(RawStr::new(input)); + assert!(result.is_err()); + result.unwrap_err() + }; + assert_eq!(errors.len(), expected_errors.len()); + for (error, expected) in errors.iter().zip(&expected_errors) { + match (&error.kind, &expected.kind) { + (ErrorKind::Unknown, ErrorKind::Unknown) => (), + (kind_a, kind_b) => assert_eq!(kind_a, kind_b), + }; + assert_eq!(error.name, expected.name); + assert_eq!(error.value, expected.value); + assert_eq!(error.entity, expected.entity); + } } #[test] fn test_empty_form() { - check_error("", "Query parameter missing", false); + check_error( + "", + vec![Error::from(ErrorKind::Missing).with_name("query")], + false, + ); } #[test] fn test_no_query() { check_error( "operation_name=foo&variables={}", - "Query parameter missing", + vec![Error::from(ErrorKind::Missing).with_name("query")], false, ); } #[test] fn test_strict() { - check_error("query=test&foo=bar", "Prohibited extra field \'foo\'", true); + check_error( + "query=test&foo=bar", + vec![Error::from(ErrorKind::Unknown).with_name("foo")], + true, + ); } #[test] fn test_duplicate_query() { check_error( "query=foo&query=bar", - "Query parameter must not occur more than once", + vec![Error::from(ErrorKind::Duplicate).with_name("query")], false, ); } @@ -345,7 +440,7 @@ mod fromform_tests { fn test_duplicate_operation_name() { check_error( "query=test&operation_name=op1&operation_name=op2", - "Operation name parameter must not occur more than once", + vec![Error::from(ErrorKind::Duplicate).with_name("operation_name")], false, ); } @@ -354,7 +449,7 @@ mod fromform_tests { fn test_duplicate_variables() { check_error( "query=test&variables={}&variables={}", - "Variables parameter must not occur more than once", + vec![Error::from(ErrorKind::Duplicate).with_name("variables")], false, ); } @@ -363,16 +458,18 @@ mod fromform_tests { fn test_variables_invalid_json() { check_error( "query=test&variables=NOT_JSON", - "expected value at line 1 column 1", + vec![Error::from(ErrorKind::Validation(Cow::Owned( + "expected value at line 1 column 1".to_owned(), + ))) + .with_name("variables")], 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); + let result: Result<GraphQLRequest, Errors> = + Form::parse_encoded(RawStr::new(r#"query=test&variables={"foo":"bar"}"#)); assert!(result.is_ok()); let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"bar"}"#).unwrap(); let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( @@ -385,10 +482,9 @@ mod fromform_tests { #[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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new( + r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#, + )); let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"x y&? z"}"#).unwrap(); let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( "test".to_string(), @@ -400,9 +496,9 @@ mod fromform_tests { #[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, _> = GraphQLRequest::from_form(&mut items, false); + let result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new( + "query=%25foo%20bar+baz%26%3F&operation_name=test", + )); assert!(result.is_ok()); let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new( "%foo bar baz&?".to_string(), @@ -415,6 +511,9 @@ mod fromform_tests { #[cfg(test)] mod tests { + + use futures; + use juniper::{ http::tests as http_tests, tests::fixtures::starwars::schema::{Database, Query}, @@ -423,30 +522,28 @@ mod tests { use rocket::{ self, get, http::ContentType, - local::{Client, LocalRequest}, - post, - request::Form, - routes, Rocket, State, + local::asynchronous::{Client, LocalResponse}, + post, routes, Build, Rocket, State, }; type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; #[get("/?<request..>")] fn get_graphql_handler( - context: State<Database>, - request: Form<super::GraphQLRequest>, - schema: State<Schema>, + context: &State<Database>, + request: super::GraphQLRequest, + schema: &State<Schema>, ) -> super::GraphQLResponse { - request.execute_sync(&schema, &context) + request.execute_sync(&*schema, &*context) } #[post("/", data = "<request>")] fn post_graphql_handler( - context: State<Database>, + context: &State<Database>, request: super::GraphQLRequest, - schema: State<Schema>, + schema: &State<Schema>, ) -> super::GraphQLResponse { - request.execute_sync(&schema, &context) + request.execute_sync(&*schema, &*context) } struct TestRocketIntegration { @@ -455,86 +552,90 @@ mod tests { impl http_tests::HttpIntegration for TestRocketIntegration { fn get(&self, url: &str) -> http_tests::TestResponse { - let req = &self.client.get(url); - make_test_response(req) + let req = self.client.get(url); + let req = futures::executor::block_on(req.dispatch()); + futures::executor::block_on(make_test_response(req)) } 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) + 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 + let req = self .client .post(url) .header(ContentType::new("application", "graphql")) .body(body); - make_test_response(req) + let req = futures::executor::block_on(req.dispatch()); + futures::executor::block_on(make_test_response(req)) } } - #[test] - fn test_rocket_integration() { + #[rocket::async_test] + async fn test_rocket_integration() { let rocket = make_rocket(); - let client = Client::new(rocket).expect("valid rocket"); + let client = Client::untracked(rocket).await.expect("valid rocket"); let integration = TestRocketIntegration { client }; http_tests::run_http_test_suite(&integration); } - #[test] - fn test_operation_names() { + #[rocket::async_test] + async fn test_operation_names() { #[post("/", data = "<request>")] fn post_graphql_assert_operation_name_handler( - context: State<Database>, + context: &State<Database>, request: super::GraphQLRequest, - schema: State<Schema>, + schema: &State<Schema>, ) -> super::GraphQLResponse { assert_eq!(request.operation_names(), vec![Some("TestQuery")]); - request.execute_sync(&schema, &context) + request.execute_sync(&*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 client = Client::untracked(rocket).await.expect("valid rocket"); - let req = client + let resp = client .post("/") .header(ContentType::JSON) - .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#); - let resp = make_test_response(&req); + .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#) + .dispatch() + .await; + let resp = make_test_response(resp); - assert_eq!(resp.status_code, 200); + assert_eq!(resp.await.status_code, 200); } - fn make_rocket() -> Rocket { + fn make_rocket() -> Rocket<Build> { 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( + fn make_rocket_without_routes() -> Rocket<Build> { + Rocket::build().manage(Database::new()).manage(Schema::new( Query, EmptyMutation::<Database>::new(), EmptySubscription::<Database>::new(), )) } - fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse { - let mut response = request.clone().dispatch(); + async fn make_test_response(response: LocalResponse<'_>) -> http_tests::TestResponse { 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(); + .into_string() + .await + .expect("No body returned from GraphQL handler"); http_tests::TestResponse { status_code, - body, + body: Some(body), content_type, } } diff --git a/juniper_rocket/tests/custom_response_tests.rs b/juniper_rocket/tests/custom_response_tests.rs index c4de3e26..050be566 100644 --- a/juniper_rocket/tests/custom_response_tests.rs +++ b/juniper_rocket/tests/custom_response_tests.rs @@ -1,6 +1,5 @@ -use rocket::http::Status; - use juniper_rocket::GraphQLResponse; +use rocket::http::Status; #[test] fn test_graphql_response_is_public() { diff --git a/juniper_rocket_async/.gitignore b/juniper_rocket_async/.gitignore deleted file mode 100644 index a9d37c56..00000000 --- a/juniper_rocket_async/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -Cargo.lock diff --git a/juniper_rocket_async/CHANGELOG.md b/juniper_rocket_async/CHANGELOG.md deleted file mode 100644 index d2889166..00000000 --- a/juniper_rocket_async/CHANGELOG.md +++ /dev/null @@ -1,62 +0,0 @@ -# master - -- 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) - -- 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 deleted file mode 100644 index 68a5fa3c..00000000 --- a/juniper_rocket_async/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "juniper_rocket_async" -version = "0.5.1" -edition = "2018" -authors = [ - "Magnus Hallin <mhallin@fastmail.com>", - "Christoph Herzog <chris@theduke.at>", -] -description = "Juniper GraphQL integration with Rocket" -license = "BSD-2-Clause" -documentation = "https://docs.rs/juniper_rocket" -repository = "https://github.com/graphql-rust/juniper" - -[dependencies] -futures = "0.3.1" -juniper = { version = "0.15.6", path = "../juniper", default-features = false } -rocket = { version = "0.5.0-rc.1", default-features = false } -serde_json = "1.0.2" - -[dev-dependencies] -juniper = { version = "0.15.6", path = "../juniper", features = ["expose-test-schema"] } diff --git a/juniper_rocket_async/LICENSE b/juniper_rocket_async/LICENSE deleted file mode 100644 index 0ccd1e17..00000000 --- a/juniper_rocket_async/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -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/README.md b/juniper_rocket_async/README.md deleted file mode 100644 index 9ff6f6e2..00000000 --- a/juniper_rocket_async/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 deleted file mode 100644 index fa86fddf..00000000 --- a/juniper_rocket_async/examples/rocket_server.rs +++ /dev/null @@ -1,48 +0,0 @@ -use juniper::{ - tests::fixtures::starwars::schema::{Database, Query}, - EmptyMutation, EmptySubscription, RootNode, -}; -use rocket::{response::content, Rocket, State}; - -type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; - -#[rocket::get("/")] -fn graphiql() -> content::Html<String> { - juniper_rocket_async::graphiql_source("/graphql", None) -} - -#[rocket::get("/graphql?<request>")] -fn get_graphql_handler( - context: &State<Database>, - request: juniper_rocket_async::GraphQLRequest, - schema: &State<Schema>, -) -> juniper_rocket_async::GraphQLResponse { - request.execute_sync(&*schema, &*context) -} - -#[rocket::post("/graphql", data = "<request>")] -fn post_graphql_handler( - context: &State<Database>, - request: juniper_rocket_async::GraphQLRequest, - schema: &State<Schema>, -) -> juniper_rocket_async::GraphQLResponse { - request.execute_sync(&*schema, &*context) -} - -#[rocket::main] -async fn main() { - Rocket::build() - .manage(Database::new()) - .manage(Schema::new( - Query, - EmptyMutation::<Database>::new(), - EmptySubscription::<Database>::new(), - )) - .mount( - "/", - rocket::routes![graphiql, get_graphql_handler, post_graphql_handler], - ) - .launch() - .await - .expect("server to launch"); -} diff --git a/juniper_rocket_async/src/lib.rs b/juniper_rocket_async/src/lib.rs deleted file mode 100644 index 4d2ab9b1..00000000 --- a/juniper_rocket_async/src/lib.rs +++ /dev/null @@ -1,643 +0,0 @@ -/*! - -# juniper_rocket_async - -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_async -[example]: https://github.com/graphql-rust/juniper_rocket_async/blob/master/examples/rocket_server.rs - -*/ - -#![doc(html_root_url = "https://docs.rs/juniper_rocket_async/0.2.0")] - -use std::{borrow::Cow, io::Cursor}; - -use rocket::{ - data::{self, FromData, ToByteUnit}, - form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField}, - http::{ContentType, Status}, - outcome::Outcome::{Failure, Forward, Success}, - response::{self, content, Responder, Response}, - Data, Request, -}; - -use juniper::{ - http::{self, GraphQLBatchRequest}, - DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, - InputValue, RootNode, ScalarValue, -}; - -/// 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<S = DefaultScalarValue>(GraphQLBatchRequest<S>) -where - S: ScalarValue; - -/// 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, - subscriptions_endpoint_url: Option<&str>, -) -> content::Html<String> { - content::Html(juniper::http::graphiql::graphiql_source( - graphql_endpoint_url, - subscriptions_endpoint_url, - )) -} - -/// Generate an HTML page containing GraphQL Playground -pub fn playground_source( - graphql_endpoint_url: &str, - subscriptions_endpoint_url: Option<&str>, -) -> content::Html<String> { - content::Html(juniper::http::playground::playground_source( - graphql_endpoint_url, - subscriptions_endpoint_url, - )) -} - -impl<S> GraphQLRequest<S> -where - S: ScalarValue, -{ - /// Synchronously execute an incoming GraphQL query. - pub fn execute_sync<CtxT, QueryT, MutationT, SubscriptionT>( - &self, - root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>, - context: &CtxT, - ) -> GraphQLResponse - where - QueryT: GraphQLType<S, Context = CtxT>, - MutationT: GraphQLType<S, Context = CtxT>, - SubscriptionT: GraphQLType<S, Context = CtxT>, - { - let response = self.0.execute_sync(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. - pub async fn execute<CtxT, QueryT, MutationT, SubscriptionT>( - &self, - root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>, - context: &CtxT, - ) -> GraphQLResponse - where - QueryT: GraphQLTypeAsync<S, Context = CtxT>, - QueryT::TypeInfo: Sync, - MutationT: GraphQLTypeAsync<S, Context = CtxT>, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT>, - SubscriptionT::TypeInfo: Sync, - CtxT: Sync, - S: Send + Sync, - { - let response = self.0.execute(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<Option<&str>> { - self.0.operation_names() - } -} - -impl GraphQLResponse { - /// Constructs an error response outside of the normal execution flow - /// - /// # Examples - /// - /// ``` - /// # use rocket::http::CookieJar; - /// # use rocket::form::Form; - /// # use rocket::response::content; - /// # use rocket::State; - /// # - /// # use juniper::tests::fixtures::starwars::schema::{Database, Query}; - /// # use juniper::{EmptyMutation, EmptySubscription, FieldError, RootNode, Value}; - /// # - /// # type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; - /// # - /// #[rocket::get("/graphql?<request..>")] - /// fn get_graphql_handler( - /// cookies: &CookieJar, - /// context: &State<Database>, - /// request: juniper_rocket_async::GraphQLRequest, - /// schema: &State<Schema>, - /// ) -> juniper_rocket_async::GraphQLResponse { - /// if cookies.get("user_id").is_none() { - /// let err = FieldError::new("User is not logged in", Value::null()); - /// return juniper_rocket_async::GraphQLResponse::error(err); - /// } - /// - /// request.execute_sync(&*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_sync(..). - pub fn custom(status: Status, response: serde_json::Value) -> Self { - let json = serde_json::to_string(&response).unwrap(); - GraphQLResponse(status, json) - } -} - -pub struct GraphQLContext<'f, S: ScalarValue> { - opts: Options, - query: Option<String>, - operation_name: Option<String>, - variables: Option<InputValue<S>>, - errors: Errors<'f>, -} - -impl<'f, S: ScalarValue> GraphQLContext<'f, S> { - fn query(&mut self, value: String) { - if self.query.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("query"); - - self.errors.push(error) - } else { - println!("{}", value); - self.query = Some(value); - } - } - - fn operation_name(&mut self, value: String) { - if self.operation_name.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("operation_name"); - - self.errors.push(error) - } else { - self.operation_name = Some(value); - } - } - - fn variables(&mut self, value: String) { - if self.variables.is_some() { - let error = Error::from(ErrorKind::Duplicate).with_name("variables"); - - self.errors.push(error) - } else { - let parse_result = serde_json::from_str::<InputValue<S>>(&value); - - match parse_result { - Ok(variables) => self.variables = Some(variables), - Err(e) => { - let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string()))) - .with_name("variables"); - - self.errors.push(error); - } - } - } - } -} - -#[rocket::async_trait] -impl<'f, S> FromForm<'f> for GraphQLRequest<S> -where - S: ScalarValue + Send, -{ - type Context = GraphQLContext<'f, S>; - - fn init(opts: Options) -> Self::Context { - GraphQLContext { - opts, - query: None, - operation_name: None, - variables: None, - errors: Errors::new(), - } - } - - fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) { - match field.name.key().map(|key| key.as_str()) { - Some("query") => ctx.query(field.value.to_owned()), - Some("operation_name") => ctx.operation_name(field.value.to_owned()), - Some("variables") => ctx.variables(field.value.to_owned()), - Some(key) => { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unknown).with_name(key); - - ctx.errors.push(error) - } - } - None => { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unexpected); - - ctx.errors.push(error) - } - } - } - } - - async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) { - if ctx.opts.strict { - let error = Error::from(ErrorKind::Unexpected).with_name(field.name); - - ctx.errors.push(error) - } - } - - fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> { - if ctx.query.is_none() { - let error = Error::from(ErrorKind::Missing).with_name("query"); - - ctx.errors.push(error) - } - - match ctx.errors.is_empty() { - true => Ok(GraphQLRequest(GraphQLBatchRequest::Single( - http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), - ))), - false => Err(ctx.errors), - } - } -} - -const BODY_LIMIT: u64 = 1024 * 100; - -#[rocket::async_trait] -impl<'r, S> FromData<'r> for GraphQLRequest<S> -where - S: ScalarValue, -{ - type Error = String; - - async fn from_data( - req: &'r Request<'_>, - data: Data<'r>, - ) -> data::Outcome<'r, Self, Self::Error> { - use rocket::tokio::io::AsyncReadExt as _; - - 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) }).await, - }; - - Box::pin(async move { - let mut body = String::new(); - let mut reader = data.open(BODY_LIMIT.bytes()); - if let Err(e) = reader.read_to_string(&mut body).await { - return Failure((Status::InternalServerError, format!("{:?}", e))); - } - - Success(GraphQLRequest(if is_json { - match serde_json::from_str(&body) { - Ok(req) => req, - Err(e) => return Failure((Status::BadRequest, format!("{}", e))), - } - } else { - GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) - })) - }) - .await - } -} - -impl<'r, 'o: 'r> Responder<'r, 'o> for GraphQLResponse { - fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> { - let GraphQLResponse(status, body) = self; - - Response::build() - .header(ContentType::new("application", "json")) - .status(status) - .sized_body(body.len(), Cursor::new(body)) - .ok() - } -} - -#[cfg(test)] -mod fromform_tests { - use super::*; - use juniper::InputValue; - use rocket::{ - form::{error::ErrorKind, Error, Form, Strict}, - http::RawStr, - }; - use std::borrow::Cow; - - fn check_error(input: &str, expected_errors: Vec<Error>, strict: bool) { - let errors = if strict { - let result = Form::<Strict<GraphQLRequest>>::parse_encoded(RawStr::new(input)); - assert!(result.is_err()); - result.unwrap_err() - } else { - let result = Form::<GraphQLRequest>::parse_encoded(RawStr::new(input)); - assert!(result.is_err()); - result.unwrap_err() - }; - assert_eq!(errors.len(), expected_errors.len()); - for (error, expected) in errors.iter().zip(&expected_errors) { - match (&error.kind, &expected.kind) { - (ErrorKind::Unknown, ErrorKind::Unknown) => (), - (kind_a, kind_b) => assert_eq!(kind_a, kind_b), - }; - assert_eq!(error.name, expected.name); - assert_eq!(error.value, expected.value); - assert_eq!(error.entity, expected.entity); - } - } - - #[test] - fn test_empty_form() { - check_error( - "", - vec![Error::from(ErrorKind::Missing).with_name("query")], - false, - ); - } - - #[test] - fn test_no_query() { - check_error( - "operation_name=foo&variables={}", - vec![Error::from(ErrorKind::Missing).with_name("query")], - false, - ); - } - - #[test] - fn test_strict() { - check_error( - "query=test&foo=bar", - vec![Error::from(ErrorKind::Unknown).with_name("foo")], - true, - ); - } - - #[test] - fn test_duplicate_query() { - check_error( - "query=foo&query=bar", - vec![Error::from(ErrorKind::Duplicate).with_name("query")], - false, - ); - } - - #[test] - fn test_duplicate_operation_name() { - check_error( - "query=test&operation_name=op1&operation_name=op2", - vec![Error::from(ErrorKind::Duplicate).with_name("operation_name")], - false, - ); - } - - #[test] - fn test_duplicate_variables() { - check_error( - "query=test&variables={}&variables={}", - vec![Error::from(ErrorKind::Duplicate).with_name("variables")], - false, - ); - } - - #[test] - fn test_variables_invalid_json() { - check_error( - "query=test&variables=NOT_JSON", - vec![Error::from(ErrorKind::Validation(Cow::Owned( - "expected value at line 1 column 1".to_owned(), - ))) - .with_name("variables")], - false, - ); - } - - #[test] - fn test_variables_valid_json() { - let result: Result<GraphQLRequest, Errors> = - Form::parse_encoded(RawStr::new(r#"query=test&variables={"foo":"bar"}"#)); - assert!(result.is_ok()); - let variables = ::serde_json::from_str::<InputValue>(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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new( - r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#, - )); - let variables = ::serde_json::from_str::<InputValue>(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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new( - "query=%25foo%20bar+baz%26%3F&operation_name=test", - )); - 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 futures; - - use juniper::{ - http::tests as http_tests, - tests::fixtures::starwars::schema::{Database, Query}, - EmptyMutation, EmptySubscription, RootNode, - }; - use rocket::{ - self, get, - http::ContentType, - local::asynchronous::{Client, LocalResponse}, - post, routes, Build, Rocket, State, - }; - - type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>; - - #[get("/?<request..>")] - fn get_graphql_handler( - context: &State<Database>, - request: super::GraphQLRequest, - schema: &State<Schema>, - ) -> super::GraphQLResponse { - request.execute_sync(&*schema, &*context) - } - - #[post("/", data = "<request>")] - fn post_graphql_handler( - context: &State<Database>, - request: super::GraphQLRequest, - schema: &State<Schema>, - ) -> super::GraphQLResponse { - request.execute_sync(&*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); - let req = futures::executor::block_on(req.dispatch()); - futures::executor::block_on(make_test_response(req)) - } - - 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)) - } - } - - #[rocket::async_test] - async fn test_rocket_integration() { - let rocket = make_rocket(); - let client = Client::untracked(rocket).await.expect("valid rocket"); - let integration = TestRocketIntegration { client }; - - http_tests::run_http_test_suite(&integration); - } - - #[rocket::async_test] - async fn test_operation_names() { - #[post("/", data = "<request>")] - fn post_graphql_assert_operation_name_handler( - context: &State<Database>, - request: super::GraphQLRequest, - schema: &State<Schema>, - ) -> super::GraphQLResponse { - assert_eq!(request.operation_names(), vec![Some("TestQuery")]); - request.execute_sync(&*schema, &*context) - } - - let rocket = make_rocket_without_routes() - .mount("/", routes![post_graphql_assert_operation_name_handler]); - let client = Client::untracked(rocket).await.expect("valid rocket"); - - let resp = client - .post("/") - .header(ContentType::JSON) - .body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#) - .dispatch() - .await; - let resp = make_test_response(resp); - - assert_eq!(resp.await.status_code, 200); - } - - fn make_rocket() -> Rocket<Build> { - make_rocket_without_routes().mount("/", routes![post_graphql_handler, get_graphql_handler]) - } - - fn make_rocket_without_routes() -> Rocket<Build> { - Rocket::build().manage(Database::new()).manage(Schema::new( - Query, - EmptyMutation::<Database>::new(), - EmptySubscription::<Database>::new(), - )) - } - - async fn make_test_response(response: LocalResponse<'_>) -> http_tests::TestResponse { - 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 - .into_string() - .await - .expect("No body returned from GraphQL handler"); - - http_tests::TestResponse { - status_code, - body: Some(body), - content_type, - } - } -} diff --git a/juniper_rocket_async/tests/custom_response_tests.rs b/juniper_rocket_async/tests/custom_response_tests.rs deleted file mode 100644 index 3cee40c9..00000000 --- a/juniper_rocket_async/tests/custom_response_tests.rs +++ /dev/null @@ -1,7 +0,0 @@ -use juniper_rocket_async::GraphQLResponse; -use rocket::http::Status; - -#[test] -fn test_graphql_response_is_public() { - let _ = GraphQLResponse(Status::Unauthorized, "Unauthorized".to_string()); -}