Merge branch 'master' of github.com-koxiaet:async-graphql/async-graphql into master

This commit is contained in:
Koxiaet 2020-09-22 20:03:33 +01:00
commit 203ea4ad64
29 changed files with 824 additions and 432 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "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"

View File

@ -1,6 +1,6 @@
[package]
name = "graphql-benchmark"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "chat"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["Ivan Plesskih <terma95@gmail.com>"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "simple"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["Ivan Plesskih <terma95@gmail.com>"]
edition = "2018"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "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"

View File

@ -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<Schema>,
request: GQLRequest,
) -> web::Json<GQLResponse> {
web::Json(GQLResponse(schema.execute(request.into_inner()).await)
request: Request,
) -> web::Json<Response> {
web::Json(Response(schema.execute(request.into_inner()).await)
}
```

View File

@ -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<Schema>,
request: GQLRequest,
) -> web::Json<GQLResponse> {
web::Json(GQLResponse(schema.execute(request.into_inner()).await)
request: Request,
) -> web::Json<Response> {
web::Json(Response(schema.execute(request.into_inner()).await)
}
```

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-actix-web"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "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"

View File

@ -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<Box<dyn Future<Output = Result<BatchRequest, Error>>>>;
type Config = MultipartOptions;
fn from_request(req: &HttpRequest, payload: &mut Payload<PayloadStream>) -> Self::Future {
let config = req.app_data::<Self::Config>().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<async_graphql::BatchResponse> for BatchResponse {
fn from(resp: async_graphql::BatchResponse) -> Self {
BatchResponse(resp)
}
}
impl Responder for BatchResponse {
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
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);
}
}
}

View File

@ -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, Request, Response};
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 `GQLRequest::into_inner` unwrap it to `async_graphql::Request`.
/// `async_graphql::http::MultipartOptions` allows to configure extraction process.
pub struct GQLRequest(Request);
impl GQLRequest {
/// Unwraps the value to `async_graphql::Request`.
pub fn into_inner(self) -> Request {
self.0
}
}
impl FromRequest for GQLRequest {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<GQLRequest, Error>>>>;
type Config = MultipartOptions;
fn from_request(req: &HttpRequest, payload: &mut Payload<PayloadStream>) -> Self::Future {
let config = req.app_data::<Self::Config>().cloned().unwrap_or_default();
if req.method() == Method::GET {
let res = web::Query::<async_graphql::Request>::from_query(req.query_string());
Box::pin(async move {
let gql_request = res?;
Ok(GQLRequest(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(GQLRequest(
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 GQLResponse(Response);
impl From<Response> for GQLResponse {
fn from(resp: Response) -> Self {
GQLResponse(resp)
}
}
impl Responder for GQLResponse {
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
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: &Response) {
if resp.is_ok() {
if let Some(cache_control) = resp.cache_control.value() {
builder.header("cache-control", cache_control);
}
}
}

View File

@ -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<Box<dyn Future<Output = Result<Request, Error>>>>;
type Config = MultipartOptions;
fn from_request(req: &HttpRequest, payload: &mut Payload<PayloadStream>) -> Self::Future {
let config = req.app_data::<Self::Config>().cloned().unwrap_or_default();
if req.method() == Method::GET {
let res = web::Query::<async_graphql::Request>::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<async_graphql::Response> for Response {
fn from(resp: async_graphql::Response) -> Self {
Response(resp)
}
}
impl Responder for Response {
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
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);
}
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-rocket"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
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"

View File

@ -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<QueryRoot, EmptyMutation, EmptySubscription>;
@ -43,13 +43,13 @@ use yansi::Paint;
/// }
///
/// #[rocket::post("/?<query..>")]
/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: GQLRequest) -> Result<GQLResponse, Status> {
/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: Request) -> Result<Response, Status> {
/// query.execute(&schema)
/// .await
/// }
///
/// #[rocket::post("/", data = "<request>", format = "application/json")]
/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: GQLRequest) -> Result<GQLResponse, Status> {
/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: Request) -> Result<Response, Status> {
/// request.execute(&schema)
/// .await
/// }
@ -133,23 +133,23 @@ impl GraphQL {
///
/// ```rust,no_run,ignore
/// #[rocket::post("/?<query..>")]
/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: GQLRequest) -> Result<GQLResponse, Status> {
/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: Request) -> Result<Response, Status> {
/// query.execute(&schema)
/// .await
/// }
///
/// #[rocket::post("/", data = "<request>", format = "application/json")]
/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: GQLRequest) -> Result<GQLResponse, Status> {
/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: Request) -> Result<Response, Status> {
/// 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<Q, M, S>(self, schema: &Schema<Q, M, S>) -> Result<GQLResponse, Status>
pub async fn execute<Q, M, S>(self, schema: &Schema<Q, M, S>) -> Result<Response, Status>
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<Self, Self::Error> {
@ -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<Self, Self::Error> {
async fn from_data(req: &RocketRequest<'_>, data: Data) -> data::Outcome<Self, Self::Error> {
let opts = match req.guard::<State<'_, Arc<MultipartOptions>>>().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))

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-tide"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["vkill <vkill.net@gmail.com>"]
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"

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-warp"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "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"

View File

@ -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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
opts: MultipartOptions,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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::<String>("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<MultipartOptions>, 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<async_graphql::BatchResponse> 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
}
}

View File

@ -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 {}

View File

@ -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;
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](<https://github.com/async-graphql/examples/blob/master/warp/starwars/src/main.rs>)*
///
/// ```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<QueryRoot, EmptyMutation, EmptySubscription>;
///
/// #[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>(GQLResponse::from(schema.execute(request).await))
/// });
/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
/// }
/// ```
pub fn graphql<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
opts: MultipartOptions,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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::<String>("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<MultipartOptions>,
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<Item = String> {
/// 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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + 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<Data>>(
schema, None,
)
}
/// GraphQL subscription filter
///
/// Specifies that a function converts the init payload to data.
pub fn graphql_subscription_with_data<Query, Mutation, Subscription, F>(
schema: Schema<Query, Mutation, Subscription>,
initializer: Option<F>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
F: FnOnce(serde_json::Value) -> FieldResult<Data> + 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<Query, Mutation, Subscription>, initializer: Option<F>| {
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 GQLResponse(async_graphql::Response);
impl From<async_graphql::Response> for GQLResponse {
fn from(resp: async_graphql::Response) -> Self {
GQLResponse(resp)
}
}
fn add_cache_control(http_resp: &mut Response, 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 GQLResponse {
fn into_response(self) -> Response {
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};

View File

@ -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](<https://github.com/async-graphql/examples/blob/master/warp/starwars/src/main.rs>)*
///
/// ```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<QueryRoot, EmptyMutation, EmptySubscription>;
///
/// #[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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
opts: MultipartOptions,
) -> impl Filter<
Extract = ((
Schema<Query, Mutation, Subscription>,
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::<String>("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<MultipartOptions>,
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<async_graphql::Response> 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
}
}

View File

@ -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<Item = String> {
/// 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<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + 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<Data>>(
schema, None,
)
}
/// GraphQL subscription filter
///
/// Specifies that a function converts the init payload to data.
pub fn graphql_subscription_with_data<Query, Mutation, Subscription, F>(
schema: Schema<Query, Mutation, Subscription>,
initializer: Option<F>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
F: FnOnce(serde_json::Value) -> FieldResult<Data> + 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<Query, Mutation, Subscription>, initializer: Option<F>| {
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"))
}

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-parser"
version = "2.0.0-alpha.13"
version = "2.0.0-alpha.14"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
description = "GraphQL query parser for async-graphql"

View File

@ -4,7 +4,7 @@
//! <!-- CI -->
//! <img src="https://github.com/async-graphql/async-graphql/workflows/CI/badge.svg" />
//! <!-- codecov -->
// <img src="https://codecov.io/gh/async-graphql/async-graphql/branch/master/graph/badge.svg" />
//! <img src="https://codecov.io/gh/async-graphql/async-graphql/branch/master/graph/badge.svg" />
//! <!-- Crates version -->
//! <a href="https://crates.io/crates/async-graphql">
//! <img src="https://img.shields.io/crates/v/async-graphql.svg?style=flat-square"
@ -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;

View File

@ -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<String>,
/// 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<Box<dyn Fn() -> 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<F: Fn() -> E + Send + Sync + 'static, E: Extension>(
mut self,
extension_factory: F,
) -> Self {
self.extensions
.push(Box::new(move || Box::new(extension_factory())));
self
}
}
impl<T: Into<String>> From<T> for Request {
@ -90,7 +109,7 @@ impl<T: Into<String>> From<T> for Request {
/// Batch support for GraphQL requests, which is either a single query, or an array of queries
///
/// **Reference:** <https://www.apollographql.com/blog/batching-client-graphql-queries-a685f5bcd41b/>
#[derive(Debug, Deserialize)]
#[derive(Deserialize)]
#[serde(untagged)]
pub enum BatchRequest {
/// Single query

View File

@ -320,6 +320,7 @@ where
self.0
.extensions
.iter()
.chain(request.extensions.iter())
.map(|factory| factory())
.collect_vec(),
));

View File

@ -1,4 +1,5 @@
use crate::{registry, InputValueResult, InputValueType, Type, Value};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`.
@ -45,12 +46,19 @@ use std::borrow::Cow;
/// }
/// ```
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
pub enum MaybeUndefined<T> {
Undefined,
Null,
Value(T),
}
impl<T> Default for MaybeUndefined<T> {
fn default() -> Self {
Self::Undefined
}
}
impl<T> MaybeUndefined<T> {
/// Returns true if the MaybeUndefined<T> is undefined.
#[inline]
@ -133,15 +141,107 @@ impl<T: InputValueType> InputValueType for MaybeUndefined<T> {
}
}
impl<T: Serialize> Serialize for MaybeUndefined<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
match self {
MaybeUndefined::Value(value) => value.serialize(serializer),
_ => serializer.serialize_none(),
}
}
}
impl<'de, T> Deserialize<'de> for MaybeUndefined<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<MaybeUndefined<T>, D::Error>
where
D: Deserializer<'de>,
{
Option::<T>::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::<i32>::type_name(), "Int");
assert_eq!(MaybeUndefined::<i32>::qualified_type_name(), "Int");
assert_eq!(&MaybeUndefined::<i32>::type_name(), "Int");
assert_eq!(&MaybeUndefined::<i32>::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::<MaybeUndefined<i32>>("100").unwrap(),
MaybeUndefined::Value(100)
);
assert_eq!(
serde_json::from_str::<MaybeUndefined<i32>>("null").unwrap(),
MaybeUndefined::Null
);
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
struct A {
a: MaybeUndefined<i32>,
}
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::<A>(r#"{"a":100}"#).unwrap(),
A {
a: MaybeUndefined::Value(100i32)
}
);
assert_eq!(
serde_json::from_str::<A>(r#"{"a":null}"#).unwrap(),
A {
a: MaybeUndefined::Null
}
);
assert_eq!(
serde_json::from_str::<A>(r#"{}"#).unwrap(),
A {
a: MaybeUndefined::Null
}
);
}
}

View File

@ -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() {

View File

@ -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 {

View File

@ -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: &registry::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 {

View File

@ -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,
})
);
}