From d3f5c56c6d35b496d2ae45e0aaa5d5fc14388ae9 Mon Sep 17 00:00:00 2001 From: William Myers Date: Sat, 19 Sep 2020 14:10:54 -0700 Subject: [PATCH 01/10] Add more trait implementations to MaybeUndefined Closes #267 --- src/types/maybe_undefined.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 4fdef844..3451269d 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -1,4 +1,5 @@ use crate::{registry, InputValueResult, InputValueType, Type, Value}; +use serde::{Deserialize, Serialize}; use std::borrow::Cow; /// Similar to `Option`, but it has three states, `undefined`, `null` and `x`. @@ -45,6 +46,7 @@ use std::borrow::Cow; /// } /// ``` #[allow(missing_docs)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum MaybeUndefined { Undefined, Null, From 5bfb992ad7c8a70eee18ce853a343904f8ee094d Mon Sep 17 00:00:00 2001 From: Sunli Date: Sun, 20 Sep 2020 13:28:18 +0800 Subject: [PATCH 02/10] Fix enum strings in query variables are not converted to proper enums. #270 --- .../rules/arguments_of_correct_type.rs | 27 +++++------ .../rules/default_values_of_correct_type.rs | 1 - src/validation/utils.rs | 22 ++++++--- tests/variables.rs | 48 +++++++++++++++++++ 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/validation/rules/arguments_of_correct_type.rs b/src/validation/rules/arguments_of_correct_type.rs index 2c9e3f5d..dd27a0a9 100644 --- a/src/validation/rules/arguments_of_correct_type.rs +++ b/src/validation/rules/arguments_of_correct_type.rs @@ -68,7 +68,6 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { if let Some(reason) = value.and_then(|value| { is_valid_input_value( ctx.registry, - ctx.variables, &arg.ty, &value, QueryPathNode { @@ -525,19 +524,19 @@ mod tests { ); } - #[test] - fn string_into_enum() { - expect_fails_rule!( - factory, - r#" - { - dog { - doesKnowCommand(dogCommand: "SIT") - } - } - "#, - ); - } + // #[test] + // fn string_into_enum() { + // expect_fails_rule!( + // factory, + // r#" + // { + // dog { + // doesKnowCommand(dogCommand: "SIT") + // } + // } + // "#, + // ); + // } #[test] fn boolean_into_enum() { diff --git a/src/validation/rules/default_values_of_correct_type.rs b/src/validation/rules/default_values_of_correct_type.rs index cf94125b..c3346c75 100644 --- a/src/validation/rules/default_values_of_correct_type.rs +++ b/src/validation/rules/default_values_of_correct_type.rs @@ -20,7 +20,6 @@ impl<'a> Visitor<'a> for DefaultValuesOfCorrectType { )); } else if let Some(reason) = is_valid_input_value( ctx.registry, - ctx.variables, &variable_definition.node.var_type.to_string(), &value.node, QueryPathNode { diff --git a/src/validation/utils.rs b/src/validation/utils.rs index cc520ac7..604e7786 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -1,6 +1,6 @@ use crate::context::QueryPathNode; use crate::parser::types::{ConstValue, Value}; -use crate::{registry, QueryPathSegment, Variables}; +use crate::{registry, QueryPathSegment}; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -36,7 +36,6 @@ fn referenced_variables_to_vec<'a>(value: &'a Value, vars: &mut Vec<&'a str>) { pub fn is_valid_input_value( registry: ®istry::Registry, - variables: Option<&Variables>, type_name: &str, value: &ConstValue, path_node: QueryPathNode, @@ -47,13 +46,12 @@ pub fn is_valid_input_value( &path_node, format!("expected type \"{}\"", type_name), )), - _ => is_valid_input_value(registry, variables, type_name, value, path_node), + _ => is_valid_input_value(registry, type_name, value, path_node), }, registry::MetaTypeName::List(type_name) => match value { ConstValue::List(elems) => elems.iter().enumerate().find_map(|(idx, elem)| { is_valid_input_value( registry, - variables, type_name, elem, QueryPathNode { @@ -62,7 +60,7 @@ pub fn is_valid_input_value( }, ) }), - _ => is_valid_input_value(registry, variables, type_name, value, path_node), + _ => is_valid_input_value(registry, type_name, value, path_node), }, registry::MetaTypeName::Named(type_name) => { if let ConstValue::Null = value { @@ -98,6 +96,19 @@ pub fn is_valid_input_value( None } } + ConstValue::String(name) => { + if !enum_values.contains_key(name.as_str()) { + Some(valid_error( + &path_node, + format!( + "enumeration type \"{}\" does not contain the value \"{}\"", + enum_name, name + ), + )) + } else { + None + } + } _ => Some(valid_error( &path_node, format!("expected type \"{}\"", type_name), @@ -131,7 +142,6 @@ pub fn is_valid_input_value( if let Some(reason) = is_valid_input_value( registry, - variables, &field.ty, value, QueryPathNode { diff --git a/tests/variables.rs b/tests/variables.rs index 29974c1c..72af1de5 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -223,3 +223,51 @@ pub async fn test_variable_in_input_object() { ); } } + +#[async_std::test] +pub async fn test_variables_enum() { + #[derive(Enum, Eq, PartialEq, Copy, Clone)] + enum MyEnum { + A, + B, + C, + } + + struct QueryRoot; + + #[Object] + impl QueryRoot { + pub async fn value(&self, value: MyEnum) -> i32 { + match value { + MyEnum::A => 1, + MyEnum::B => 2, + MyEnum::C => 3, + } + } + } + + let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); + let query = Request::new( + r#" + query QueryWithVariables($value1: MyEnum, $value2: MyEnum, $value3: MyEnum) { + a: value(value: $value1) + b: value(value: $value2) + c: value(value: $value3) + } + "#, + ) + .variables(Variables::from_json(serde_json::json!({ + "value1": "A", + "value2": "B", + "value3": "C", + }))); + + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + serde_json::json!({ + "a": 1, + "b": 2, + "c": 3, + }) + ); +} From a9cb78108437948b8d1347551679fde62512bd59 Mon Sep 17 00:00:00 2001 From: Sunli Date: Sun, 20 Sep 2020 13:31:46 +0800 Subject: [PATCH 03/10] 2.0.0-alpha.14 --- Cargo.toml | 6 +++--- benchmark/Cargo.toml | 2 +- benchmark/chat/Cargo.toml | 2 +- benchmark/simple/Cargo.toml | 2 +- derive/Cargo.toml | 4 ++-- integrations/actix-web/Cargo.toml | 4 ++-- integrations/rocket/Cargo.toml | 4 ++-- integrations/tide/Cargo.toml | 4 ++-- integrations/warp/Cargo.toml | 4 ++-- parser/Cargo.toml | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e2923dd..e62ff5a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" description = "A GraphQL server library implemented in Rust" @@ -22,8 +22,8 @@ unblock = ["blocking"] nightly = [] [dependencies] -async-graphql-derive = { path = "derive", version = "2.0.0-alpha.13" } -async-graphql-parser = { path = "parser", version = "2.0.0-alpha.13" } +async-graphql-derive = { path = "derive", version = "2.0.0-alpha.14" } +async-graphql-parser = { path = "parser", version = "2.0.0-alpha.14" } async-stream = "0.3" async-trait = "0.1.30" diff --git a/benchmark/Cargo.toml b/benchmark/Cargo.toml index bfd966b9..8e8d40f1 100644 --- a/benchmark/Cargo.toml +++ b/benchmark/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-benchmark" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" diff --git a/benchmark/chat/Cargo.toml b/benchmark/chat/Cargo.toml index e84b814f..ff1a2382 100644 --- a/benchmark/chat/Cargo.toml +++ b/benchmark/chat/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chat" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["Ivan Plesskih "] edition = "2018" diff --git a/benchmark/simple/Cargo.toml b/benchmark/simple/Cargo.toml index ac5b1a8b..ff9308bf 100644 --- a/benchmark/simple/Cargo.toml +++ b/benchmark/simple/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["Ivan Plesskih "] edition = "2018" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 481540dd..99e937a1 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" description = "Macros for async-graphql" @@ -16,7 +16,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "2.0.0-alpha.13" } +async-graphql-parser = { path = "../parser", version = "2.0.0-alpha.14" } proc-macro2 = "1.0.6" syn = { version = "1.0.20", features = ["full", "extra-traits"] } quote = "1.0.3" diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 5d0f7e2a..3bd92b40 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" description = "async-graphql for actix-web" @@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "2.0.0-alpha.13" } +async-graphql = { path = "../..", version = "2.0.0-alpha.14" } actix-web = "3.0.0" actix-web-actors = "3.0.0" actix-http = "2.0.0" diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index bd682523..13083b8e 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["Daniel Wiesenberg "] edition = "2018" description = "async-graphql for Rocket.rs" @@ -14,7 +14,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "2.0.0-alpha.13" } +async-graphql = { path = "../..", version = "2.0.0-alpha.14" } rocket = { git = "https://github.com/SergioBenitez/Rocket/", rev = "dc2c6ec", default-features = false } #TODO: Change to Cargo crate, when Rocket 0.5.0 is released log = "0.4.11" yansi = "0.5.0" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index 698a10a4..5a590494 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["vkill "] edition = "2018" description = "async-graphql for tide" @@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "2.0.0-alpha.13" } +async-graphql = { path = "../..", version = "2.0.0-alpha.14" } tide = "0.13.0" async-trait = "0.1.36" serde_json = "1.0.56" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index 38d2d065..c3b31bf7 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" description = "async-graphql for warp" @@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "2.0.0-alpha.13" } +async-graphql = { path = "../..", version = "2.0.0-alpha.14" } warp = "0.2.2" futures = "0.3.0" bytes = "0.5.4" diff --git a/parser/Cargo.toml b/parser/Cargo.toml index d872344c..66fff41d 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "2.0.0-alpha.13" +version = "2.0.0-alpha.14" authors = ["sunli ", "Koxiaet"] edition = "2018" description = "GraphQL query parser for async-graphql" From 7bb67f135eaf4cd5a13dc0a7fc64d7196a08fca6 Mon Sep 17 00:00:00 2001 From: Sunli Date: Sun, 20 Sep 2020 13:34:49 +0800 Subject: [PATCH 04/10] Fix docs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 245a0cae..1263f488 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! //! //! -// +//! //! //! //! Date: Sun, 20 Sep 2020 13:44:20 +0800 Subject: [PATCH 05/10] Remove all GQL prefix. #208 --- docs/en/src/integrations_to_actix_web.md | 10 +++--- docs/zh-CN/src/integrations_to_actix_web.md | 8 ++--- integrations/actix-web/src/lib.rs | 30 ++++++++-------- integrations/rocket/src/lib.rs | 40 ++++++++++----------- integrations/warp/src/lib.rs | 16 ++++----- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/en/src/integrations_to_actix_web.md b/docs/en/src/integrations_to_actix_web.md index c9b91181..f327f68e 100644 --- a/docs/en/src/integrations_to_actix_web.md +++ b/docs/en/src/integrations_to_actix_web.md @@ -1,7 +1,7 @@ # Actix-web -`Async-graphql-actix-web` provides an implementation of `actix_web::FromRequest` for `GQLRequest`. -This is actually an abstraction around `async_graphql::Request` and you can call `GQLRequest::into_inner` to +`Async-graphql-actix-web` provides an implementation of `actix_web::FromRequest` for `Request`. +This is actually an abstraction around `async_graphql::Request` and you can call `Request::into_inner` to convert it into a `async_graphql::Request`. `WSSubscription` is an Actor that supports WebSocket subscriptions. @@ -14,9 +14,9 @@ When you define your `actix_web::App` you need to pass in the Schema as data. async fn index( // Schema now accessible here schema: web::Data, - request: GQLRequest, -) -> web::Json { - web::Json(GQLResponse(schema.execute(request.into_inner()).await) + request: Request, +) -> web::Json { + web::Json(Response(schema.execute(request.into_inner()).await) } ``` diff --git a/docs/zh-CN/src/integrations_to_actix_web.md b/docs/zh-CN/src/integrations_to_actix_web.md index e9fe480d..3ceed975 100644 --- a/docs/zh-CN/src/integrations_to_actix_web.md +++ b/docs/zh-CN/src/integrations_to_actix_web.md @@ -1,6 +1,6 @@ # Actix-web -`Async-graphql-actix-web`提供实现了`actix_web::FromRequest`的`GQLRequest`,它其实是`async_graphql::Request`的包装,你可以调用`GQLRequest::into_inner`把它转换成一个`async_graphql::Request`。 +`Async-graphql-actix-web`提供实现了`actix_web::FromRequest`的`Request`,它其实是`async_graphql::Request`的包装,你可以调用`Request::into_inner`把它转换成一个`async_graphql::Request`。 `WSSubscription`是一个支持Web Socket订阅的Actor。 @@ -11,9 +11,9 @@ ```rust async fn index( schema: web::Data, - request: GQLRequest, -) -> web::Json { - web::Json(GQLResponse(schema.execute(request.into_inner()).await) + request: Request, +) -> web::Json { + web::Json(Response(schema.execute(request.into_inner()).await) } ``` diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index f9186f88..086901f6 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -9,7 +9,7 @@ use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream}; use actix_web::http::StatusCode; use actix_web::{http, web, Error, FromRequest, HttpRequest, HttpResponse, Responder}; use async_graphql::http::MultipartOptions; -use async_graphql::{ParseRequestError, Request, Response}; +use async_graphql::ParseRequestError; use futures::channel::mpsc; use futures::future::Ready; use futures::io::ErrorKind; @@ -21,20 +21,20 @@ pub use subscription::WSSubscription; /// Extractor for GraphQL request /// -/// It's a wrapper of `async_graphql::Request`, you can use `GQLRequest::into_inner` unwrap it to `async_graphql::Request`. +/// It's a wrapper of `async_graphql::Request`, you can use `Request::into_inner` unwrap it to `async_graphql::Request`. /// `async_graphql::http::MultipartOptions` allows to configure extraction process. -pub struct GQLRequest(Request); +pub struct Request(async_graphql::Request); -impl GQLRequest { +impl Request { /// Unwraps the value to `async_graphql::Request`. - pub fn into_inner(self) -> Request { + pub fn into_inner(self) -> async_graphql::Request { self.0 } } -impl FromRequest for GQLRequest { +impl FromRequest for Request { type Error = Error; - type Future = Pin>>>; + type Future = Pin>>>; type Config = MultipartOptions; fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -44,7 +44,7 @@ impl FromRequest for GQLRequest { let res = web::Query::::from_query(req.query_string()); Box::pin(async move { let gql_request = res?; - Ok(GQLRequest(gql_request.into_inner())) + Ok(Request(gql_request.into_inner())) }) } else { let content_type = req @@ -66,7 +66,7 @@ impl FromRequest for GQLRequest { }); Box::pin(async move { - Ok(GQLRequest( + Ok(Request( async_graphql::http::receive_body( content_type, rx.map_err(|err| io::Error::new(ErrorKind::Other, err)) @@ -87,15 +87,15 @@ impl FromRequest for GQLRequest { } /// Responder for GraphQL response -pub struct GQLResponse(Response); +pub struct Response(async_graphql::Response); -impl From for GQLResponse { - fn from(resp: Response) -> Self { - GQLResponse(resp) +impl From for Response { + fn from(resp: async_graphql::Response) -> Self { + Response(resp) } } -impl Responder for GQLResponse { +impl Responder for Response { type Error = Error; type Future = Ready>; @@ -108,7 +108,7 @@ impl Responder for GQLResponse { } } -fn add_cache_control(builder: &mut HttpResponseBuilder, resp: &Response) { +fn add_cache_control(builder: &mut HttpResponseBuilder, resp: &async_graphql::Response) { if resp.is_ok() { if let Some(cache_control) = resp.cache_control.value() { builder.header("cache-control", cache_control); diff --git a/integrations/rocket/src/lib.rs b/integrations/rocket/src/lib.rs index bce9b6b7..38e1758f 100644 --- a/integrations/rocket/src/lib.rs +++ b/integrations/rocket/src/lib.rs @@ -13,7 +13,7 @@ use rocket::{ http::{ContentType, Header, Status}, request::{self, FromQuery, Outcome}, response::{self, Responder, ResponseBuilder}, - Request, Response, State, + Request as RocketRequest, Response as RocketResponse, State, }; use std::{io::Cursor, sync::Arc}; use tokio_util::compat::Tokio02AsyncReadCompatExt; @@ -28,7 +28,7 @@ use yansi::Paint; /// ```rust,no_run /// /// use async_graphql::{EmptyMutation, EmptySubscription, Schema, Object}; -/// use async_graphql_rocket::{GQLRequest, GraphQL, GQLResponse}; +/// use async_graphql_rocket::{Request, GraphQL, Response}; /// use rocket::{response::content, routes, State, http::Status}; /// /// type ExampleSchema = Schema; @@ -43,13 +43,13 @@ use yansi::Paint; /// } /// /// #[rocket::post("/?")] -/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: GQLRequest) -> Result { +/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: Request) -> Result { /// query.execute(&schema) /// .await /// } /// /// #[rocket::post("/", data = "", format = "application/json")] -/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: GQLRequest) -> Result { +/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: Request) -> Result { /// request.execute(&schema) /// .await /// } @@ -133,23 +133,23 @@ impl GraphQL { /// /// ```rust,no_run,ignore /// #[rocket::post("/?")] -/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: GQLRequest) -> Result { +/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: Request) -> Result { /// query.execute(&schema) /// .await /// } /// /// #[rocket::post("/", data = "", format = "application/json")] -/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: GQLRequest) -> Result { +/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: Request) -> Result { /// request.execute(&schema) /// .await /// } /// ``` -pub struct GQLRequest(pub async_graphql::Request); +pub struct Request(pub async_graphql::Request); -impl GQLRequest { +impl Request { /// Mimics `async_graphql::Schema.execute()`. /// Executes the query, always return a complete result. - pub async fn execute(self, schema: &Schema) -> Result + pub async fn execute(self, schema: &Schema) -> Result where Q: ObjectType + Send + Sync + 'static, M: ObjectType + Send + Sync + 'static, @@ -159,7 +159,7 @@ impl GQLRequest { .execute(self.0) .await .into_result() - .map(GQLResponse) + .map(Response) .map_err(|e| { error!("{}", e); Status::BadRequest @@ -167,7 +167,7 @@ impl GQLRequest { } } -impl<'q> FromQuery<'q> for GQLRequest { +impl<'q> FromQuery<'q> for Request { type Error = String; fn from_query(query_items: request::Query) -> Result { @@ -222,7 +222,7 @@ impl<'q> FromQuery<'q> for GQLRequest { request = request.operation_name(operation_name); } - Ok(GQLRequest(request)) + Ok(Request(request)) } else { Err(r#"Parameter "query" missing from request."#.to_string()) } @@ -230,10 +230,10 @@ impl<'q> FromQuery<'q> for GQLRequest { } #[rocket::async_trait] -impl FromData for GQLRequest { +impl FromData for Request { type Error = String; - async fn from_data(req: &Request<'_>, data: Data) -> data::Outcome { + async fn from_data(req: &RocketRequest<'_>, data: Data) -> data::Outcome { let opts = match req.guard::>>().await { Outcome::Success(opts) => opts, Outcome::Failure(_) => { @@ -255,22 +255,22 @@ impl FromData for GQLRequest { .await; match request { - Ok(request) => data::Outcome::Success(GQLRequest(request)), + Ok(request) => data::Outcome::Success(Request(request)), Err(e) => data::Outcome::Failure((Status::BadRequest, format!("{}", e))), } } } /// Wrapper around `async-graphql::Response` for implementing the trait -/// `rocket::response::responder::Responder`, so that `GQLResponse` can directly be returned +/// `rocket::response::responder::Responder`, so that `Response` can directly be returned /// from a Rocket Route function. -pub struct GQLResponse(pub async_graphql::Response); +pub struct Response(pub async_graphql::Response); -impl<'r> Responder<'r, 'static> for GQLResponse { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { +impl<'r> Responder<'r, 'static> for Response { + fn respond_to(self, _: &'r RocketRequest<'_>) -> response::Result<'static> { let body = serde_json::to_string(&self.0).unwrap(); - Response::build() + RocketResponse::build() .header(ContentType::new("application", "json")) .status(Status::Ok) .sized_body(body.len(), Cursor::new(body)) diff --git a/integrations/warp/src/lib.rs b/integrations/warp/src/lib.rs index 1221290a..21f3e169 100644 --- a/integrations/warp/src/lib.rs +++ b/integrations/warp/src/lib.rs @@ -15,7 +15,7 @@ use std::io::{self, ErrorKind}; use std::sync::Arc; use warp::filters::ws; use warp::reject::Reject; -use warp::reply::Response; +use warp::reply::Response as WarpResponse; use warp::{Buf, Filter, Rejection, Reply}; /// Bad request error @@ -63,7 +63,7 @@ impl Reject for BadRequest {} /// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); /// let filter = async_graphql_warp::graphql(schema). /// and_then(|(schema, request): (MySchema, async_graphql::Request)| async move { -/// Ok::<_, Infallible>(GQLResponse::from(schema.execute(request).await)) +/// Ok::<_, Infallible>(async_graphql_warp::Response::from(schema.execute(request).await)) /// }); /// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await; /// } @@ -226,15 +226,15 @@ where } /// GraphQL reply -pub struct GQLResponse(async_graphql::Response); +pub struct Response(async_graphql::Response); -impl From for GQLResponse { +impl From for Response { fn from(resp: async_graphql::Response) -> Self { - GQLResponse(resp) + Response(resp) } } -fn add_cache_control(http_resp: &mut Response, resp: &async_graphql::Response) { +fn add_cache_control(http_resp: &mut WarpResponse, resp: &async_graphql::Response) { if resp.is_ok() { if let Some(cache_control) = resp.cache_control.value() { if let Ok(value) = cache_control.parse() { @@ -244,8 +244,8 @@ fn add_cache_control(http_resp: &mut Response, resp: &async_graphql::Response) { } } -impl Reply for GQLResponse { - fn into_response(self) -> Response { +impl Reply for Response { + fn into_response(self) -> WarpResponse { let mut resp = warp::reply::with_header( warp::reply::json(&self.0), "content-type", From e21fe02cacad9c761b6f25ed809a6ae8dccb9811 Mon Sep 17 00:00:00 2001 From: Sunli Date: Sun, 20 Sep 2020 15:47:17 +0800 Subject: [PATCH 06/10] Make ObjectType is public. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 1263f488..ea441c9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,7 @@ pub use look_ahead::Lookahead; pub use parser::types::{ConstValue as Value, Number}; pub use registry::CacheControl; pub use request::{BatchRequest, Request}; +pub use resolver_utils::ObjectType; pub use response::{BatchResponse, Response}; pub use schema::{Schema, SchemaBuilder, SchemaEnv}; pub use validation::ValidationMode; From 90b2cfdaeab77763ec1f28369bfec148a84d0648 Mon Sep 17 00:00:00 2001 From: William Myers Date: Sun, 20 Sep 2020 13:22:51 -0700 Subject: [PATCH 07/10] Remove Deserialize impl from MaybeUndefined Change Serialize implementation to use the `untagged` attribute. This handles the common case of directly serializing `MaybeUndefined`, but deserializing into an `Option`. We could add a more sophisticated custom deserialize implementation later. --- src/types/maybe_undefined.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 3451269d..b3e4a9d2 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -46,7 +46,8 @@ use std::borrow::Cow; /// } /// ``` #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Clone, Debug)] +#[serde(untagged)] pub enum MaybeUndefined { Undefined, Null, From 37e21c54fa8f7b7d27cea29530ded694ad550b6d Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 21 Sep 2020 14:57:33 +0800 Subject: [PATCH 08/10] Add BatchRequest support for actixweb and warp integrations. --- integrations/actix-web/src/batch_request.rs | 100 ++++++++ integrations/actix-web/src/lib.rs | 114 +-------- integrations/actix-web/src/request.rs | 109 ++++++++ integrations/warp/src/batch_request.rs | 101 ++++++++ integrations/warp/src/error.rs | 14 ++ integrations/warp/src/lib.rs | 259 +------------------- integrations/warp/src/request.rs | 147 +++++++++++ integrations/warp/src/subscription.rs | 93 +++++++ src/types/maybe_undefined.rs | 2 +- 9 files changed, 577 insertions(+), 362 deletions(-) create mode 100644 integrations/actix-web/src/batch_request.rs create mode 100644 integrations/actix-web/src/request.rs create mode 100644 integrations/warp/src/batch_request.rs create mode 100644 integrations/warp/src/error.rs create mode 100644 integrations/warp/src/request.rs create mode 100644 integrations/warp/src/subscription.rs diff --git a/integrations/actix-web/src/batch_request.rs b/integrations/actix-web/src/batch_request.rs new file mode 100644 index 00000000..dbba8260 --- /dev/null +++ b/integrations/actix-web/src/batch_request.rs @@ -0,0 +1,100 @@ +use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream}; +use actix_web::http::StatusCode; +use actix_web::{http, web, Error, FromRequest, HttpRequest, HttpResponse, Responder}; +use async_graphql::http::MultipartOptions; +use async_graphql::ParseRequestError; +use futures::channel::mpsc; +use futures::future::Ready; +use futures::io::ErrorKind; +use futures::{Future, SinkExt, StreamExt, TryFutureExt, TryStreamExt}; +use std::io; +use std::pin::Pin; + +/// Extractor for GraphQL batch request +/// +/// It's a wrapper of `async_graphql::Request`, you can use `Request::into_inner` unwrap it to `async_graphql::Request`. +/// `async_graphql::http::MultipartOptions` allows to configure extraction process. +pub struct BatchRequest(async_graphql::BatchRequest); + +impl BatchRequest { + /// Unwraps the value to `async_graphql::Request`. + pub fn into_inner(self) -> async_graphql::BatchRequest { + self.0 + } +} + +impl FromRequest for BatchRequest { + type Error = Error; + type Future = Pin>>>; + type Config = MultipartOptions; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let config = req.app_data::().cloned().unwrap_or_default(); + + let content_type = req + .headers() + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map(|value| value.to_string()); + + let (mut tx, rx) = mpsc::channel(16); + + // Because Payload is !Send, so forward it to mpsc::Sender + let mut payload = web::Payload(payload.take()); + actix_rt::spawn(async move { + while let Some(item) = payload.next().await { + if tx.send(item).await.is_err() { + return; + } + } + }); + + Box::pin(async move { + Ok(BatchRequest( + async_graphql::http::receive_batch_body( + content_type, + rx.map_err(|err| io::Error::new(ErrorKind::Other, err)) + .into_async_read(), + config, + ) + .map_err(|err| match err { + ParseRequestError::PayloadTooLarge => { + actix_web::error::ErrorPayloadTooLarge(err) + } + _ => actix_web::error::ErrorBadRequest(err), + }) + .await?, + )) + }) + } +} + +/// Responder for GraphQL batch response +pub struct BatchResponse(async_graphql::BatchResponse); + +impl From for BatchResponse { + fn from(resp: async_graphql::BatchResponse) -> Self { + BatchResponse(resp) + } +} + +impl Responder for BatchResponse { + type Error = Error; + type Future = Ready>; + + fn respond_to(self, _req: &HttpRequest) -> Self::Future { + let mut res = HttpResponse::build(StatusCode::OK); + res.content_type("application/json"); + add_cache_control(&mut res, &self.0); + let res = res.body(serde_json::to_string(&self.0).unwrap()); + futures::future::ok(res) + } +} + +fn add_cache_control(builder: &mut HttpResponseBuilder, resp: &async_graphql::BatchResponse) { + if resp.is_ok() { + if let Some(cache_control) = resp.cache_control().value() { + builder.header("cache-control", cache_control); + } + } +} diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index 086901f6..4a918274 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -1,117 +1,11 @@ //! Async-graphql integration with Actix-web -#![warn(missing_docs)] #![forbid(unsafe_code)] +mod batch_request; +mod request; mod subscription; -use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream}; -use actix_web::http::StatusCode; -use actix_web::{http, web, Error, FromRequest, HttpRequest, HttpResponse, Responder}; -use async_graphql::http::MultipartOptions; -use async_graphql::ParseRequestError; -use futures::channel::mpsc; -use futures::future::Ready; -use futures::io::ErrorKind; -use futures::{Future, SinkExt, StreamExt, TryFutureExt, TryStreamExt}; -use http::Method; -use std::io; -use std::pin::Pin; +pub use batch_request::{BatchRequest, BatchResponse}; +pub use request::{Request, Response}; pub use subscription::WSSubscription; - -/// Extractor for GraphQL request -/// -/// It's a wrapper of `async_graphql::Request`, you can use `Request::into_inner` unwrap it to `async_graphql::Request`. -/// `async_graphql::http::MultipartOptions` allows to configure extraction process. -pub struct Request(async_graphql::Request); - -impl Request { - /// Unwraps the value to `async_graphql::Request`. - pub fn into_inner(self) -> async_graphql::Request { - self.0 - } -} - -impl FromRequest for Request { - type Error = Error; - type Future = Pin>>>; - type Config = MultipartOptions; - - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let config = req.app_data::().cloned().unwrap_or_default(); - - if req.method() == Method::GET { - let res = web::Query::::from_query(req.query_string()); - Box::pin(async move { - let gql_request = res?; - Ok(Request(gql_request.into_inner())) - }) - } else { - let content_type = req - .headers() - .get(http::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .map(|value| value.to_string()); - - let (mut tx, rx) = mpsc::channel(16); - - // Because Payload is !Send, so forward it to mpsc::Sender - let mut payload = web::Payload(payload.take()); - actix_rt::spawn(async move { - while let Some(item) = payload.next().await { - if tx.send(item).await.is_err() { - return; - } - } - }); - - Box::pin(async move { - Ok(Request( - async_graphql::http::receive_body( - content_type, - rx.map_err(|err| io::Error::new(ErrorKind::Other, err)) - .into_async_read(), - config, - ) - .map_err(|err| match err { - ParseRequestError::PayloadTooLarge => { - actix_web::error::ErrorPayloadTooLarge(err) - } - _ => actix_web::error::ErrorBadRequest(err), - }) - .await?, - )) - }) - } - } -} - -/// Responder for GraphQL response -pub struct Response(async_graphql::Response); - -impl From for Response { - fn from(resp: async_graphql::Response) -> Self { - Response(resp) - } -} - -impl Responder for Response { - type Error = Error; - type Future = Ready>; - - fn respond_to(self, _req: &HttpRequest) -> Self::Future { - let mut res = HttpResponse::build(StatusCode::OK); - res.content_type("application/json"); - add_cache_control(&mut res, &self.0); - let res = res.body(serde_json::to_string(&self.0).unwrap()); - futures::future::ok(res) - } -} - -fn add_cache_control(builder: &mut HttpResponseBuilder, resp: &async_graphql::Response) { - if resp.is_ok() { - if let Some(cache_control) = resp.cache_control.value() { - builder.header("cache-control", cache_control); - } - } -} diff --git a/integrations/actix-web/src/request.rs b/integrations/actix-web/src/request.rs new file mode 100644 index 00000000..e2b2ce2a --- /dev/null +++ b/integrations/actix-web/src/request.rs @@ -0,0 +1,109 @@ +use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream}; +use actix_web::http::StatusCode; +use actix_web::{http, web, Error, FromRequest, HttpRequest, HttpResponse, Responder}; +use async_graphql::http::MultipartOptions; +use async_graphql::ParseRequestError; +use futures::channel::mpsc; +use futures::future::Ready; +use futures::io::ErrorKind; +use futures::{Future, SinkExt, StreamExt, TryFutureExt, TryStreamExt}; +use http::Method; +use std::io; +use std::pin::Pin; + +/// Extractor for GraphQL request +/// +/// It's a wrapper of `async_graphql::Request`, you can use `Request::into_inner` unwrap it to `async_graphql::Request`. +/// `async_graphql::http::MultipartOptions` allows to configure extraction process. +pub struct Request(async_graphql::Request); + +impl Request { + /// Unwraps the value to `async_graphql::Request`. + pub fn into_inner(self) -> async_graphql::Request { + self.0 + } +} + +impl FromRequest for Request { + type Error = Error; + type Future = Pin>>>; + type Config = MultipartOptions; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let config = req.app_data::().cloned().unwrap_or_default(); + + if req.method() == Method::GET { + let res = web::Query::::from_query(req.query_string()); + Box::pin(async move { + let gql_request = res?; + Ok(Request(gql_request.into_inner())) + }) + } else { + let content_type = req + .headers() + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .map(|value| value.to_string()); + + let (mut tx, rx) = mpsc::channel(16); + + // Because Payload is !Send, so forward it to mpsc::Sender + let mut payload = web::Payload(payload.take()); + actix_rt::spawn(async move { + while let Some(item) = payload.next().await { + if tx.send(item).await.is_err() { + return; + } + } + }); + + Box::pin(async move { + Ok(Request( + async_graphql::http::receive_body( + content_type, + rx.map_err(|err| io::Error::new(ErrorKind::Other, err)) + .into_async_read(), + config, + ) + .map_err(|err| match err { + ParseRequestError::PayloadTooLarge => { + actix_web::error::ErrorPayloadTooLarge(err) + } + _ => actix_web::error::ErrorBadRequest(err), + }) + .await?, + )) + }) + } + } +} + +/// Responder for GraphQL response +pub struct Response(async_graphql::Response); + +impl From for Response { + fn from(resp: async_graphql::Response) -> Self { + Response(resp) + } +} + +impl Responder for Response { + type Error = Error; + type Future = Ready>; + + fn respond_to(self, _req: &HttpRequest) -> Self::Future { + let mut res = HttpResponse::build(StatusCode::OK); + res.content_type("application/json"); + add_cache_control(&mut res, &self.0); + let res = res.body(serde_json::to_string(&self.0).unwrap()); + futures::future::ok(res) + } +} + +fn add_cache_control(builder: &mut HttpResponseBuilder, resp: &async_graphql::Response) { + if resp.is_ok() { + if let Some(cache_control) = resp.cache_control.value() { + builder.header("cache-control", cache_control); + } + } +} diff --git a/integrations/warp/src/batch_request.rs b/integrations/warp/src/batch_request.rs new file mode 100644 index 00000000..49ae56b2 --- /dev/null +++ b/integrations/warp/src/batch_request.rs @@ -0,0 +1,101 @@ +use crate::BadRequest; +use async_graphql::http::MultipartOptions; +use async_graphql::{ObjectType, Schema, SubscriptionType}; +use futures::TryStreamExt; +use std::io; +use std::io::ErrorKind; +use std::sync::Arc; +use warp::reply::Response as WarpResponse; +use warp::{Buf, Filter, Rejection, Reply}; + +/// GraphQL batch request filter +/// +/// It outputs a tuple containing the `async_graphql::Schema` and `async_graphql::BatchRequest`. +pub fn graphql_batch( + schema: Schema, +) -> impl Filter< + Extract = (( + Schema, + async_graphql::BatchRequest, + ),), + Error = Rejection, +> + Clone +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, +{ + graphql_batch_opts(schema, Default::default()) +} + +/// Similar to graphql_batch, but you can set the options `async_graphql::MultipartOptions`. +pub fn graphql_batch_opts( + schema: Schema, + opts: MultipartOptions, +) -> impl Filter< + Extract = (( + Schema, + async_graphql::BatchRequest, + ),), + Error = Rejection, +> + Clone +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, +{ + let opts = Arc::new(opts); + warp::any() + .and(warp::header::optional::("content-type")) + .and(warp::body::stream()) + .and(warp::any().map(move || opts.clone())) + .and(warp::any().map(move || schema.clone())) + .and_then( + |content_type, body, opts: Arc, schema| async move { + let request = async_graphql::http::receive_batch_body( + content_type, + futures::TryStreamExt::map_err(body, |err| { + io::Error::new(ErrorKind::Other, err) + }) + .map_ok(|mut buf| Buf::to_bytes(&mut buf)) + .into_async_read(), + MultipartOptions::clone(&opts), + ) + .await + .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; + Ok::<_, Rejection>((schema, request)) + }, + ) +} + +/// Reply for `async_graphql::BatchRequest`. +pub struct BatchResponse(async_graphql::BatchResponse); + +impl From for BatchResponse { + fn from(resp: async_graphql::BatchResponse) -> Self { + BatchResponse(resp) + } +} + +fn add_cache_control(http_resp: &mut WarpResponse, resp: &async_graphql::BatchResponse) { + if resp.is_ok() { + if let Some(cache_control) = resp.cache_control().value() { + if let Ok(value) = cache_control.parse() { + http_resp.headers_mut().insert("cache-control", value); + } + } + } +} + +impl Reply for BatchResponse { + fn into_response(self) -> WarpResponse { + let mut resp = warp::reply::with_header( + warp::reply::json(&self.0), + "content-type", + "application/json", + ) + .into_response(); + add_cache_control(&mut resp, &self.0); + resp + } +} diff --git a/integrations/warp/src/error.rs b/integrations/warp/src/error.rs new file mode 100644 index 00000000..6b540c7f --- /dev/null +++ b/integrations/warp/src/error.rs @@ -0,0 +1,14 @@ +use warp::reject::Reject; + +/// Bad request error +/// +/// It's a wrapper of `async_graphql::ParseRequestError`. +pub struct BadRequest(pub anyhow::Error); + +impl std::fmt::Debug for BadRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Reject for BadRequest {} diff --git a/integrations/warp/src/lib.rs b/integrations/warp/src/lib.rs index 21f3e169..22d9ea18 100644 --- a/integrations/warp/src/lib.rs +++ b/integrations/warp/src/lib.rs @@ -1,258 +1,15 @@ //! Async-graphql integration with Warp -#![warn(missing_docs)] #![allow(clippy::type_complexity)] #![allow(clippy::needless_doctest_main)] #![forbid(unsafe_code)] -use async_graphql::http::MultipartOptions; -use async_graphql::{ - resolver_utils::ObjectType, Data, FieldResult, Request, Schema, SubscriptionType, -}; -use futures::{future, StreamExt, TryStreamExt}; -use hyper::Method; -use std::io::{self, ErrorKind}; -use std::sync::Arc; -use warp::filters::ws; -use warp::reject::Reject; -use warp::reply::Response as WarpResponse; -use warp::{Buf, Filter, Rejection, Reply}; +mod batch_request; +mod error; +mod request; +mod subscription; -/// Bad request error -/// -/// It's a wrapper of `async_graphql::ParseRequestError`. -pub struct BadRequest(pub anyhow::Error); - -impl std::fmt::Debug for BadRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Reject for BadRequest {} - -/// GraphQL request filter -/// -/// It outputs a tuple containing the `async_graphql::Schema` and `async_graphql::Request`. -/// -/// # Examples -/// -/// *[Full Example]()* -/// -/// ```no_run -/// -/// use async_graphql::*; -/// use async_graphql_warp::*; -/// use warp::Filter; -/// use std::convert::Infallible; -/// -/// struct QueryRoot; -/// -/// #[Object] -/// impl QueryRoot { -/// #[field] -/// async fn value(&self, ctx: &Context<'_>) -> i32 { -/// unimplemented!() -/// } -/// } -/// -/// type MySchema = Schema; -/// -/// #[tokio::main] -/// async fn main() { -/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); -/// let filter = async_graphql_warp::graphql(schema). -/// and_then(|(schema, request): (MySchema, async_graphql::Request)| async move { -/// Ok::<_, Infallible>(async_graphql_warp::Response::from(schema.execute(request).await)) -/// }); -/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await; -/// } -/// ``` -pub fn graphql( - schema: Schema, -) -> impl Filter< - Extract = (( - Schema, - async_graphql::Request, - ),), - Error = Rejection, -> + Clone -where - Query: ObjectType + Send + Sync + 'static, - Mutation: ObjectType + Send + Sync + 'static, - Subscription: SubscriptionType + Send + Sync + 'static, -{ - graphql_opts(schema, Default::default()) -} - -/// Similar to graphql, but you can set the options `async_graphql::MultipartOptions`. -pub fn graphql_opts( - schema: Schema, - opts: MultipartOptions, -) -> impl Filter< - Extract = (( - Schema, - async_graphql::Request, - ),), - Error = Rejection, -> + Clone -where - Query: ObjectType + Send + Sync + 'static, - Mutation: ObjectType + Send + Sync + 'static, - Subscription: SubscriptionType + Send + Sync + 'static, -{ - let opts = Arc::new(opts); - warp::any() - .and(warp::method()) - .and(warp::query::raw().or(warp::any().map(String::new)).unify()) - .and(warp::header::optional::("content-type")) - .and(warp::body::stream()) - .and(warp::any().map(move || opts.clone())) - .and(warp::any().map(move || schema.clone())) - .and_then( - |method, - query: String, - content_type, - body, - opts: Arc, - schema| async move { - if method == Method::GET { - let request: Request = serde_urlencoded::from_str(&query) - .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; - Ok::<_, Rejection>((schema, request)) - } else { - let request = async_graphql::http::receive_body( - content_type, - futures::TryStreamExt::map_err(body, |err| io::Error::new(ErrorKind::Other, err)) - .map_ok(|mut buf| Buf::to_bytes(&mut buf)) - .into_async_read(), - MultipartOptions::clone(&opts), - ) - .await - .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; - Ok::<_, Rejection>((schema, request)) - } - }, - ) -} - -/// GraphQL subscription filter -/// -/// # Examples -/// -/// ```no_run -/// use async_graphql::*; -/// use async_graphql_warp::*; -/// use warp::Filter; -/// use futures::{Stream, StreamExt}; -/// use std::time::Duration; -/// -/// struct QueryRoot; -/// -/// #[Object] -/// impl QueryRoot {} -/// -/// struct SubscriptionRoot; -/// -/// #[Subscription] -/// impl SubscriptionRoot { -/// #[field] -/// async fn tick(&self) -> impl Stream { -/// tokio::time::interval(Duration::from_secs(1)).map(|n| format!("{}", n.elapsed().as_secs_f32())) -/// } -/// } -/// -/// #[tokio::main] -/// async fn main() { -/// let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot); -/// let filter = async_graphql_warp::graphql_subscription(schema) -/// .or(warp::any().map(|| "Hello, World!")); -/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await; -/// } -/// ``` -pub fn graphql_subscription( - schema: Schema, -) -> impl Filter + Clone -where - Query: ObjectType + Sync + Send + 'static, - Mutation: ObjectType + Sync + Send + 'static, - Subscription: SubscriptionType + Send + Sync + 'static, -{ - graphql_subscription_with_data::<_, _, _, fn(serde_json::Value) -> FieldResult>( - schema, None, - ) -} - -/// GraphQL subscription filter -/// -/// Specifies that a function converts the init payload to data. -pub fn graphql_subscription_with_data( - schema: Schema, - initializer: Option, -) -> impl Filter + Clone -where - Query: ObjectType + Sync + Send + 'static, - Mutation: ObjectType + Sync + Send + 'static, - Subscription: SubscriptionType + Send + Sync + 'static, - F: FnOnce(serde_json::Value) -> FieldResult + Send + Sync + Clone + 'static, -{ - warp::any() - .and(warp::ws()) - .and(warp::any().map(move || schema.clone())) - .and(warp::any().map(move || initializer.clone())) - .map( - |ws: ws::Ws, schema: Schema, initializer: Option| { - ws.on_upgrade(move |websocket| { - let (ws_sender, ws_receiver) = websocket.split(); - - async move { - let _ = async_graphql::http::WebSocket::with_data( - schema, - ws_receiver - .take_while(|msg| future::ready(msg.is_ok())) - .map(Result::unwrap) - .map(ws::Message::into_bytes), - initializer, - ) - .map(ws::Message::text) - .map(Ok) - .forward(ws_sender) - .await; - } - }) - }, - ) - .map(|reply| warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")) -} - -/// GraphQL reply -pub struct Response(async_graphql::Response); - -impl From for Response { - fn from(resp: async_graphql::Response) -> Self { - Response(resp) - } -} - -fn add_cache_control(http_resp: &mut WarpResponse, resp: &async_graphql::Response) { - if resp.is_ok() { - if let Some(cache_control) = resp.cache_control.value() { - if let Ok(value) = cache_control.parse() { - http_resp.headers_mut().insert("cache-control", value); - } - } - } -} - -impl Reply for Response { - fn into_response(self) -> WarpResponse { - let mut resp = warp::reply::with_header( - warp::reply::json(&self.0), - "content-type", - "application/json", - ) - .into_response(); - add_cache_control(&mut resp, &self.0); - resp - } -} +pub use batch_request::{graphql_batch, graphql_batch_opts, BatchResponse}; +pub use error::BadRequest; +pub use request::{graphql, graphql_opts, Response}; +pub use subscription::{graphql_subscription, graphql_subscription_with_data}; diff --git a/integrations/warp/src/request.rs b/integrations/warp/src/request.rs new file mode 100644 index 00000000..ce38c851 --- /dev/null +++ b/integrations/warp/src/request.rs @@ -0,0 +1,147 @@ +use crate::BadRequest; +use async_graphql::http::MultipartOptions; +use async_graphql::{ObjectType, Schema, SubscriptionType}; +use futures::TryStreamExt; +use std::io; +use std::io::ErrorKind; +use std::sync::Arc; +use warp::http::Method; +use warp::reply::Response as WarpResponse; +use warp::{Buf, Filter, Rejection, Reply}; + +/// GraphQL request filter +/// +/// It outputs a tuple containing the `async_graphql::Schema` and `async_graphql::Request`. +/// +/// # Examples +/// +/// *[Full Example]()* +/// +/// ```no_run +/// +/// use async_graphql::*; +/// use async_graphql_warp::*; +/// use warp::Filter; +/// use std::convert::Infallible; +/// +/// struct QueryRoot; +/// +/// #[Object] +/// impl QueryRoot { +/// #[field] +/// async fn value(&self, ctx: &Context<'_>) -> i32 { +/// unimplemented!() +/// } +/// } +/// +/// type MySchema = Schema; +/// +/// #[tokio::main] +/// async fn main() { +/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); +/// let filter = async_graphql_warp::graphql(schema). +/// and_then(|(schema, request): (MySchema, async_graphql::Request)| async move { +/// Ok::<_, Infallible>(async_graphql_warp::Response::from(schema.execute(request).await)) +/// }); +/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await; +/// } +/// ``` +pub fn graphql( + schema: Schema, +) -> impl Filter< + Extract = (( + Schema, + async_graphql::Request, + ),), + Error = Rejection, +> + Clone +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, +{ + graphql_opts(schema, Default::default()) +} + +/// Similar to graphql, but you can set the options `async_graphql::MultipartOptions`. +pub fn graphql_opts( + schema: Schema, + opts: MultipartOptions, +) -> impl Filter< + Extract = (( + Schema, + async_graphql::Request, + ),), + Error = Rejection, +> + Clone +where + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, +{ + let opts = Arc::new(opts); + warp::any() + .and(warp::method()) + .and(warp::query::raw().or(warp::any().map(String::new)).unify()) + .and(warp::header::optional::("content-type")) + .and(warp::body::stream()) + .and(warp::any().map(move || opts.clone())) + .and(warp::any().map(move || schema.clone())) + .and_then( + |method, + query: String, + content_type, + body, + opts: Arc, + schema| async move { + if method == Method::GET { + let request: async_graphql::Request = serde_urlencoded::from_str(&query) + .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; + Ok::<_, Rejection>((schema, request)) + } else { + let request = async_graphql::http::receive_body( + content_type, + futures::TryStreamExt::map_err(body, |err| io::Error::new(ErrorKind::Other, err)) + .map_ok(|mut buf| Buf::to_bytes(&mut buf)) + .into_async_read(), + MultipartOptions::clone(&opts), + ) + .await + .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; + Ok::<_, Rejection>((schema, request)) + } + }, + ) +} + +/// Reply for `async_graphql::Request`. +pub struct Response(async_graphql::Response); + +impl From for Response { + fn from(resp: async_graphql::Response) -> Self { + Response(resp) + } +} + +fn add_cache_control(http_resp: &mut WarpResponse, resp: &async_graphql::Response) { + if resp.is_ok() { + if let Some(cache_control) = resp.cache_control.value() { + if let Ok(value) = cache_control.parse() { + http_resp.headers_mut().insert("cache-control", value); + } + } + } +} + +impl Reply for Response { + fn into_response(self) -> WarpResponse { + let mut resp = warp::reply::with_header( + warp::reply::json(&self.0), + "content-type", + "application/json", + ) + .into_response(); + add_cache_control(&mut resp, &self.0); + resp + } +} diff --git a/integrations/warp/src/subscription.rs b/integrations/warp/src/subscription.rs new file mode 100644 index 00000000..1cf4fce8 --- /dev/null +++ b/integrations/warp/src/subscription.rs @@ -0,0 +1,93 @@ +use async_graphql::{resolver_utils::ObjectType, Data, FieldResult, Schema, SubscriptionType}; +use futures::{future, StreamExt}; +use warp::filters::ws; +use warp::{Filter, Rejection, Reply}; + +/// GraphQL subscription filter +/// +/// # Examples +/// +/// ```no_run +/// use async_graphql::*; +/// use async_graphql_warp::*; +/// use warp::Filter; +/// use futures::{Stream, StreamExt}; +/// use std::time::Duration; +/// +/// struct QueryRoot; +/// +/// #[Object] +/// impl QueryRoot {} +/// +/// struct SubscriptionRoot; +/// +/// #[Subscription] +/// impl SubscriptionRoot { +/// #[field] +/// async fn tick(&self) -> impl Stream { +/// tokio::time::interval(Duration::from_secs(1)).map(|n| format!("{}", n.elapsed().as_secs_f32())) +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() { +/// let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot); +/// let filter = async_graphql_warp::graphql_subscription(schema) +/// .or(warp::any().map(|| "Hello, World!")); +/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await; +/// } +/// ``` +pub fn graphql_subscription( + schema: Schema, +) -> impl Filter + Clone +where + Query: ObjectType + Sync + Send + 'static, + Mutation: ObjectType + Sync + Send + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, +{ + graphql_subscription_with_data::<_, _, _, fn(serde_json::Value) -> FieldResult>( + schema, None, + ) +} + +/// GraphQL subscription filter +/// +/// Specifies that a function converts the init payload to data. +pub fn graphql_subscription_with_data( + schema: Schema, + initializer: Option, +) -> impl Filter + Clone +where + Query: ObjectType + Sync + Send + 'static, + Mutation: ObjectType + Sync + Send + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, + F: FnOnce(serde_json::Value) -> FieldResult + Send + Sync + Clone + 'static, +{ + warp::any() + .and(warp::ws()) + .and(warp::any().map(move || schema.clone())) + .and(warp::any().map(move || initializer.clone())) + .map( + |ws: ws::Ws, schema: Schema, initializer: Option| { + ws.on_upgrade(move |websocket| { + let (ws_sender, ws_receiver) = websocket.split(); + + async move { + let _ = async_graphql::http::WebSocket::with_data( + schema, + ws_receiver + .take_while(|msg| future::ready(msg.is_ok())) + .map(Result::unwrap) + .map(ws::Message::into_bytes), + initializer, + ) + .map(ws::Message::text) + .map(Ok) + .forward(ws_sender) + .await; + } + }) + }, + ) + .map(|reply| warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")) +} diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index b3e4a9d2..68c2ad18 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -1,5 +1,5 @@ use crate::{registry, InputValueResult, InputValueType, Type, Value}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::borrow::Cow; /// Similar to `Option`, but it has three states, `undefined`, `null` and `x`. From 0259a357822eab0c393ce1306f6814aa4820eeb4 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 21 Sep 2020 15:22:45 +0800 Subject: [PATCH 09/10] Implement Serialize/Deserialize for MaybeUndefine. --- src/types/maybe_undefined.rs | 105 +++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 68c2ad18..d3175633 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -1,5 +1,5 @@ use crate::{registry, InputValueResult, InputValueType, Type, Value}; -use serde::Serialize; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; /// Similar to `Option`, but it has three states, `undefined`, `null` and `x`. @@ -46,14 +46,19 @@ use std::borrow::Cow; /// } /// ``` #[allow(missing_docs)] -#[derive(Serialize, Clone, Debug)] -#[serde(untagged)] +#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum MaybeUndefined { Undefined, Null, Value(T), } +impl Default for MaybeUndefined { + fn default() -> Self { + Self::Undefined + } +} + impl MaybeUndefined { /// Returns true if the MaybeUndefined is undefined. #[inline] @@ -136,15 +141,107 @@ impl InputValueType for MaybeUndefined { } } +impl Serialize for MaybeUndefined { + fn serialize(&self, serializer: S) -> std::result::Result { + match self { + MaybeUndefined::Value(value) => value.serialize(serializer), + _ => serializer.serialize_none(), + } + } +} + +impl<'de, T> Deserialize<'de> for MaybeUndefined +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).map(|value| match value { + Some(value) => MaybeUndefined::Value(value), + None => MaybeUndefined::Null, + }) + } +} + #[cfg(test)] mod tests { use crate::*; + use serde::{Deserialize, Serialize}; #[test] - fn test_optional_type() { + fn test_maybe_undefined_type() { assert_eq!(MaybeUndefined::::type_name(), "Int"); assert_eq!(MaybeUndefined::::qualified_type_name(), "Int"); assert_eq!(&MaybeUndefined::::type_name(), "Int"); assert_eq!(&MaybeUndefined::::qualified_type_name(), "Int"); } + + #[test] + fn test_maybe_undefined_serde() { + assert_eq!( + serde_json::to_string(&MaybeUndefined::Value(100i32)).unwrap(), + "100" + ); + + assert_eq!( + serde_json::from_str::>("100").unwrap(), + MaybeUndefined::Value(100) + ); + assert_eq!( + serde_json::from_str::>("null").unwrap(), + MaybeUndefined::Null + ); + + #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] + struct A { + a: MaybeUndefined, + } + + assert_eq!( + serde_json::to_string(&A { + a: MaybeUndefined::Value(100i32) + }) + .unwrap(), + r#"{"a":100}"# + ); + + assert_eq!( + serde_json::to_string(&A { + a: MaybeUndefined::Null, + }) + .unwrap(), + r#"{"a":null}"# + ); + + assert_eq!( + serde_json::to_string(&A { + a: MaybeUndefined::Undefined, + }) + .unwrap(), + r#"{"a":null}"# + ); + + assert_eq!( + serde_json::from_str::(r#"{"a":100}"#).unwrap(), + A { + a: MaybeUndefined::Value(100i32) + } + ); + + assert_eq!( + serde_json::from_str::(r#"{"a":null}"#).unwrap(), + A { + a: MaybeUndefined::Null + } + ); + + assert_eq!( + serde_json::from_str::(r#"{}"#).unwrap(), + A { + a: MaybeUndefined::Null + } + ); + } } From fa10a55745c4f870d5ce1419bf4217660b2e186c Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 21 Sep 2020 15:53:07 +0800 Subject: [PATCH 10/10] Add Request::extension method. #271 --- src/request.rs | 23 +++++++++++++++++++++-- src/schema.rs | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/request.rs b/src/request.rs index b8601f2c..319b49a1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,3 +1,4 @@ +use crate::extensions::{BoxExtension, Extension}; use crate::parser::types::UploadValue; use crate::{Data, ParseRequestError, Value, Variables}; use serde::{Deserialize, Deserializer}; @@ -8,22 +9,29 @@ use std::fs::File; /// /// This can be deserialized from a structure of the query string, the operation name and the /// variables. The names are all in `camelCase` (e.g. `operationName`). -#[derive(Debug, Deserialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Request { /// The query source of the request. pub query: String, + /// The operation name of the request. #[serde(default, rename = "operationName")] pub operation_name: Option, + /// The variables of the request. #[serde(default)] pub variables: Variables, + /// The data of the request that can be accessed through `Context::data`. /// /// **This data is only valid for this request** #[serde(skip)] pub data: Data, + + /// Extensions for this request. + #[serde(skip)] + pub extensions: Vec BoxExtension + Send + Sync>>, } impl Request { @@ -34,6 +42,7 @@ impl Request { operation_name: None, variables: Variables::default(), data: Data::default(), + extensions: Vec::default(), } } @@ -79,6 +88,16 @@ impl Request { content, }); } + + /// Add an extension + pub fn extension E + Send + Sync + 'static, E: Extension>( + mut self, + extension_factory: F, + ) -> Self { + self.extensions + .push(Box::new(move || Box::new(extension_factory()))); + self + } } impl> From for Request { @@ -90,7 +109,7 @@ impl> From for Request { /// Batch support for GraphQL requests, which is either a single query, or an array of queries /// /// **Reference:** -#[derive(Debug, Deserialize)] +#[derive(Deserialize)] #[serde(untagged)] pub enum BatchRequest { /// Single query diff --git a/src/schema.rs b/src/schema.rs index 4762a835..1e000db3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -315,6 +315,7 @@ where self.0 .extensions .iter() + .chain(request.extensions.iter()) .map(|factory| factory()) .collect_vec(), ));