Merge branch 'master' of github.com-koxiaet:async-graphql/async-graphql into master
This commit is contained in:
commit
203ea4ad64
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql"
|
name = "async-graphql"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "A GraphQL server library implemented in Rust"
|
description = "A GraphQL server library implemented in Rust"
|
||||||
|
@ -22,8 +22,8 @@ unblock = ["blocking"]
|
||||||
nightly = []
|
nightly = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-graphql-derive = { path = "derive", 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.13" }
|
async-graphql-parser = { path = "parser", version = "2.0.0-alpha.14" }
|
||||||
|
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
async-trait = "0.1.30"
|
async-trait = "0.1.30"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "graphql-benchmark"
|
name = "graphql-benchmark"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "chat"
|
name = "chat"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["Ivan Plesskih <terma95@gmail.com>"]
|
authors = ["Ivan Plesskih <terma95@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "simple"
|
name = "simple"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["Ivan Plesskih <terma95@gmail.com>"]
|
authors = ["Ivan Plesskih <terma95@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-derive"
|
name = "async-graphql-derive"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Macros for async-graphql"
|
description = "Macros for async-graphql"
|
||||||
|
@ -16,7 +16,7 @@ categories = ["network-programming", "asynchronous"]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
proc-macro2 = "1.0.6"
|
||||||
syn = { version = "1.0.20", features = ["full", "extra-traits"] }
|
syn = { version = "1.0.20", features = ["full", "extra-traits"] }
|
||||||
quote = "1.0.3"
|
quote = "1.0.3"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Actix-web
|
# Actix-web
|
||||||
|
|
||||||
`Async-graphql-actix-web` provides an implementation of `actix_web::FromRequest` for `GQLRequest`.
|
`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 `GQLRequest::into_inner` to
|
This is actually an abstraction around `async_graphql::Request` and you can call `Request::into_inner` to
|
||||||
convert it into a `async_graphql::Request`.
|
convert it into a `async_graphql::Request`.
|
||||||
|
|
||||||
`WSSubscription` is an Actor that supports WebSocket subscriptions.
|
`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(
|
async fn index(
|
||||||
// Schema now accessible here
|
// Schema now accessible here
|
||||||
schema: web::Data<Schema>,
|
schema: web::Data<Schema>,
|
||||||
request: GQLRequest,
|
request: Request,
|
||||||
) -> web::Json<GQLResponse> {
|
) -> web::Json<Response> {
|
||||||
web::Json(GQLResponse(schema.execute(request.into_inner()).await)
|
web::Json(Response(schema.execute(request.into_inner()).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Actix-web
|
# 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。
|
`WSSubscription`是一个支持Web Socket订阅的Actor。
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@
|
||||||
```rust
|
```rust
|
||||||
async fn index(
|
async fn index(
|
||||||
schema: web::Data<Schema>,
|
schema: web::Data<Schema>,
|
||||||
request: GQLRequest,
|
request: Request,
|
||||||
) -> web::Json<GQLResponse> {
|
) -> web::Json<Response> {
|
||||||
web::Json(GQLResponse(schema.execute(request.into_inner()).await)
|
web::Json(Response(schema.execute(request.into_inner()).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-actix-web"
|
name = "async-graphql-actix-web"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "async-graphql for actix-web"
|
description = "async-graphql for actix-web"
|
||||||
|
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[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 = "3.0.0"
|
||||||
actix-web-actors = "3.0.0"
|
actix-web-actors = "3.0.0"
|
||||||
actix-http = "2.0.0"
|
actix-http = "2.0.0"
|
||||||
|
|
100
integrations/actix-web/src/batch_request.rs
Normal file
100
integrations/actix-web/src/batch_request.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,117 +1,11 @@
|
||||||
//! Async-graphql integration with Actix-web
|
//! Async-graphql integration with Actix-web
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
mod batch_request;
|
||||||
|
mod request;
|
||||||
mod subscription;
|
mod subscription;
|
||||||
|
|
||||||
use actix_web::dev::{HttpResponseBuilder, Payload, PayloadStream};
|
pub use batch_request::{BatchRequest, BatchResponse};
|
||||||
use actix_web::http::StatusCode;
|
pub use request::{Request, Response};
|
||||||
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 subscription::WSSubscription;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
109
integrations/actix-web/src/request.rs
Normal file
109
integrations/actix-web/src/request.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-rocket"
|
name = "async-graphql-rocket"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
|
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "async-graphql for Rocket.rs"
|
description = "async-graphql for Rocket.rs"
|
||||||
|
@ -14,7 +14,7 @@ keywords = ["futures", "async", "graphql", "rocket"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[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
|
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"
|
log = "0.4.11"
|
||||||
yansi = "0.5.0"
|
yansi = "0.5.0"
|
||||||
|
|
|
@ -13,7 +13,7 @@ use rocket::{
|
||||||
http::{ContentType, Header, Status},
|
http::{ContentType, Header, Status},
|
||||||
request::{self, FromQuery, Outcome},
|
request::{self, FromQuery, Outcome},
|
||||||
response::{self, Responder, ResponseBuilder},
|
response::{self, Responder, ResponseBuilder},
|
||||||
Request, Response, State,
|
Request as RocketRequest, Response as RocketResponse, State,
|
||||||
};
|
};
|
||||||
use std::{io::Cursor, sync::Arc};
|
use std::{io::Cursor, sync::Arc};
|
||||||
use tokio_util::compat::Tokio02AsyncReadCompatExt;
|
use tokio_util::compat::Tokio02AsyncReadCompatExt;
|
||||||
|
@ -28,7 +28,7 @@ use yansi::Paint;
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
///
|
///
|
||||||
/// use async_graphql::{EmptyMutation, EmptySubscription, Schema, Object};
|
/// 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};
|
/// use rocket::{response::content, routes, State, http::Status};
|
||||||
///
|
///
|
||||||
/// type ExampleSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
|
/// type ExampleSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
|
||||||
|
@ -43,13 +43,13 @@ use yansi::Paint;
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[rocket::post("/?<query..>")]
|
/// #[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)
|
/// query.execute(&schema)
|
||||||
/// .await
|
/// .await
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[rocket::post("/", data = "<request>", format = "application/json")]
|
/// #[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)
|
/// request.execute(&schema)
|
||||||
/// .await
|
/// .await
|
||||||
/// }
|
/// }
|
||||||
|
@ -133,23 +133,23 @@ impl GraphQL {
|
||||||
///
|
///
|
||||||
/// ```rust,no_run,ignore
|
/// ```rust,no_run,ignore
|
||||||
/// #[rocket::post("/?<query..>")]
|
/// #[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)
|
/// query.execute(&schema)
|
||||||
/// .await
|
/// .await
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[rocket::post("/", data = "<request>", format = "application/json")]
|
/// #[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)
|
/// request.execute(&schema)
|
||||||
/// .await
|
/// .await
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct GQLRequest(pub async_graphql::Request);
|
pub struct Request(pub async_graphql::Request);
|
||||||
|
|
||||||
impl GQLRequest {
|
impl Request {
|
||||||
/// Mimics `async_graphql::Schema.execute()`.
|
/// Mimics `async_graphql::Schema.execute()`.
|
||||||
/// Executes the query, always return a complete result.
|
/// 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
|
where
|
||||||
Q: ObjectType + Send + Sync + 'static,
|
Q: ObjectType + Send + Sync + 'static,
|
||||||
M: ObjectType + Send + Sync + 'static,
|
M: ObjectType + Send + Sync + 'static,
|
||||||
|
@ -159,7 +159,7 @@ impl GQLRequest {
|
||||||
.execute(self.0)
|
.execute(self.0)
|
||||||
.await
|
.await
|
||||||
.into_result()
|
.into_result()
|
||||||
.map(GQLResponse)
|
.map(Response)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
Status::BadRequest
|
Status::BadRequest
|
||||||
|
@ -167,7 +167,7 @@ impl GQLRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q> FromQuery<'q> for GQLRequest {
|
impl<'q> FromQuery<'q> for Request {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn from_query(query_items: request::Query) -> Result<Self, Self::Error> {
|
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);
|
request = request.operation_name(operation_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GQLRequest(request))
|
Ok(Request(request))
|
||||||
} else {
|
} else {
|
||||||
Err(r#"Parameter "query" missing from request."#.to_string())
|
Err(r#"Parameter "query" missing from request."#.to_string())
|
||||||
}
|
}
|
||||||
|
@ -230,10 +230,10 @@ impl<'q> FromQuery<'q> for GQLRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl FromData for GQLRequest {
|
impl FromData for Request {
|
||||||
type Error = String;
|
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 {
|
let opts = match req.guard::<State<'_, Arc<MultipartOptions>>>().await {
|
||||||
Outcome::Success(opts) => opts,
|
Outcome::Success(opts) => opts,
|
||||||
Outcome::Failure(_) => {
|
Outcome::Failure(_) => {
|
||||||
|
@ -255,22 +255,22 @@ impl FromData for GQLRequest {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match request {
|
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))),
|
Err(e) => data::Outcome::Failure((Status::BadRequest, format!("{}", e))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper around `async-graphql::Response` for implementing the trait
|
/// 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.
|
/// 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 {
|
impl<'r> Responder<'r, 'static> for Response {
|
||||||
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
fn respond_to(self, _: &'r RocketRequest<'_>) -> response::Result<'static> {
|
||||||
let body = serde_json::to_string(&self.0).unwrap();
|
let body = serde_json::to_string(&self.0).unwrap();
|
||||||
|
|
||||||
Response::build()
|
RocketResponse::build()
|
||||||
.header(ContentType::new("application", "json"))
|
.header(ContentType::new("application", "json"))
|
||||||
.status(Status::Ok)
|
.status(Status::Ok)
|
||||||
.sized_body(body.len(), Cursor::new(body))
|
.sized_body(body.len(), Cursor::new(body))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-tide"
|
name = "async-graphql-tide"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["vkill <vkill.net@gmail.com>"]
|
authors = ["vkill <vkill.net@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "async-graphql for tide"
|
description = "async-graphql for tide"
|
||||||
|
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-graphql = { path = "../..", version = "2.0.0-alpha.13" }
|
async-graphql = { path = "../..", version = "2.0.0-alpha.14" }
|
||||||
tide = "0.13.0"
|
tide = "0.13.0"
|
||||||
async-trait = "0.1.36"
|
async-trait = "0.1.36"
|
||||||
serde_json = "1.0.56"
|
serde_json = "1.0.56"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-warp"
|
name = "async-graphql-warp"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "async-graphql for warp"
|
description = "async-graphql for warp"
|
||||||
|
@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"]
|
||||||
categories = ["network-programming", "asynchronous"]
|
categories = ["network-programming", "asynchronous"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-graphql = { path = "../..", version = "2.0.0-alpha.13" }
|
async-graphql = { path = "../..", version = "2.0.0-alpha.14" }
|
||||||
warp = "0.2.2"
|
warp = "0.2.2"
|
||||||
futures = "0.3.0"
|
futures = "0.3.0"
|
||||||
bytes = "0.5.4"
|
bytes = "0.5.4"
|
||||||
|
|
101
integrations/warp/src/batch_request.rs
Normal file
101
integrations/warp/src/batch_request.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
14
integrations/warp/src/error.rs
Normal file
14
integrations/warp/src/error.rs
Normal 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 {}
|
|
@ -1,258 +1,15 @@
|
||||||
//! Async-graphql integration with Warp
|
//! Async-graphql integration with Warp
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
|
||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
#![allow(clippy::needless_doctest_main)]
|
#![allow(clippy::needless_doctest_main)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use async_graphql::http::MultipartOptions;
|
mod batch_request;
|
||||||
use async_graphql::{
|
mod error;
|
||||||
resolver_utils::ObjectType, Data, FieldResult, Request, Schema, SubscriptionType,
|
mod request;
|
||||||
};
|
mod subscription;
|
||||||
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};
|
|
||||||
|
|
||||||
/// Bad request error
|
pub use batch_request::{graphql_batch, graphql_batch_opts, BatchResponse};
|
||||||
///
|
pub use error::BadRequest;
|
||||||
/// It's a wrapper of `async_graphql::ParseRequestError`.
|
pub use request::{graphql, graphql_opts, Response};
|
||||||
pub struct BadRequest(pub anyhow::Error);
|
pub use subscription::{graphql_subscription, graphql_subscription_with_data};
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
147
integrations/warp/src/request.rs
Normal file
147
integrations/warp/src/request.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
93
integrations/warp/src/subscription.rs
Normal file
93
integrations/warp/src/subscription.rs
Normal 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"))
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "async-graphql-parser"
|
name = "async-graphql-parser"
|
||||||
version = "2.0.0-alpha.13"
|
version = "2.0.0-alpha.14"
|
||||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "GraphQL query parser for async-graphql"
|
description = "GraphQL query parser for async-graphql"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
//! <!-- CI -->
|
//! <!-- CI -->
|
||||||
//! <img src="https://github.com/async-graphql/async-graphql/workflows/CI/badge.svg" />
|
//! <img src="https://github.com/async-graphql/async-graphql/workflows/CI/badge.svg" />
|
||||||
//! <!-- codecov -->
|
//! <!-- 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 -->
|
//! <!-- Crates version -->
|
||||||
//! <a href="https://crates.io/crates/async-graphql">
|
//! <a href="https://crates.io/crates/async-graphql">
|
||||||
//! <img src="https://img.shields.io/crates/v/async-graphql.svg?style=flat-square"
|
//! <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 parser::types::{ConstValue as Value, Number};
|
||||||
pub use registry::CacheControl;
|
pub use registry::CacheControl;
|
||||||
pub use request::{BatchRequest, Request};
|
pub use request::{BatchRequest, Request};
|
||||||
|
pub use resolver_utils::ObjectType;
|
||||||
pub use response::{BatchResponse, Response};
|
pub use response::{BatchResponse, Response};
|
||||||
pub use schema::{Schema, SchemaBuilder, SchemaEnv};
|
pub use schema::{Schema, SchemaBuilder, SchemaEnv};
|
||||||
pub use validation::ValidationMode;
|
pub use validation::ValidationMode;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::extensions::{BoxExtension, Extension};
|
||||||
use crate::parser::types::UploadValue;
|
use crate::parser::types::UploadValue;
|
||||||
use crate::{Data, ParseRequestError, Value, Variables};
|
use crate::{Data, ParseRequestError, Value, Variables};
|
||||||
use serde::{Deserialize, Deserializer};
|
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
|
/// 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`).
|
/// variables. The names are all in `camelCase` (e.g. `operationName`).
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// The query source of the request.
|
/// The query source of the request.
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
|
||||||
/// The operation name of the request.
|
/// The operation name of the request.
|
||||||
#[serde(default, rename = "operationName")]
|
#[serde(default, rename = "operationName")]
|
||||||
pub operation_name: Option<String>,
|
pub operation_name: Option<String>,
|
||||||
|
|
||||||
/// The variables of the request.
|
/// The variables of the request.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub variables: Variables,
|
pub variables: Variables,
|
||||||
|
|
||||||
/// The data of the request that can be accessed through `Context::data`.
|
/// The data of the request that can be accessed through `Context::data`.
|
||||||
///
|
///
|
||||||
/// **This data is only valid for this request**
|
/// **This data is only valid for this request**
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
|
|
||||||
|
/// Extensions for this request.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub extensions: Vec<Box<dyn Fn() -> BoxExtension + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
|
@ -34,6 +42,7 @@ impl Request {
|
||||||
operation_name: None,
|
operation_name: None,
|
||||||
variables: Variables::default(),
|
variables: Variables::default(),
|
||||||
data: Data::default(),
|
data: Data::default(),
|
||||||
|
extensions: Vec::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +88,16 @@ impl Request {
|
||||||
content,
|
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 {
|
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
|
/// 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/>
|
/// **Reference:** <https://www.apollographql.com/blog/batching-client-graphql-queries-a685f5bcd41b/>
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum BatchRequest {
|
pub enum BatchRequest {
|
||||||
/// Single query
|
/// Single query
|
||||||
|
|
|
@ -320,6 +320,7 @@ where
|
||||||
self.0
|
self.0
|
||||||
.extensions
|
.extensions
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(request.extensions.iter())
|
||||||
.map(|factory| factory())
|
.map(|factory| factory())
|
||||||
.collect_vec(),
|
.collect_vec(),
|
||||||
));
|
));
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{registry, InputValueResult, InputValueType, Type, Value};
|
use crate::{registry, InputValueResult, InputValueType, Type, Value};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`.
|
/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`.
|
||||||
|
@ -45,12 +46,19 @@ use std::borrow::Cow;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
|
||||||
pub enum MaybeUndefined<T> {
|
pub enum MaybeUndefined<T> {
|
||||||
Undefined,
|
Undefined,
|
||||||
Null,
|
Null,
|
||||||
Value(T),
|
Value(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Default for MaybeUndefined<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> MaybeUndefined<T> {
|
impl<T> MaybeUndefined<T> {
|
||||||
/// Returns true if the MaybeUndefined<T> is undefined.
|
/// Returns true if the MaybeUndefined<T> is undefined.
|
||||||
#[inline]
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_optional_type() {
|
fn test_maybe_undefined_type() {
|
||||||
assert_eq!(MaybeUndefined::<i32>::type_name(), "Int");
|
assert_eq!(MaybeUndefined::<i32>::type_name(), "Int");
|
||||||
assert_eq!(MaybeUndefined::<i32>::qualified_type_name(), "Int");
|
assert_eq!(MaybeUndefined::<i32>::qualified_type_name(), "Int");
|
||||||
assert_eq!(&MaybeUndefined::<i32>::type_name(), "Int");
|
assert_eq!(&MaybeUndefined::<i32>::type_name(), "Int");
|
||||||
assert_eq!(&MaybeUndefined::<i32>::qualified_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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,6 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> {
|
||||||
if let Some(reason) = value.and_then(|value| {
|
if let Some(reason) = value.and_then(|value| {
|
||||||
is_valid_input_value(
|
is_valid_input_value(
|
||||||
ctx.registry,
|
ctx.registry,
|
||||||
ctx.variables,
|
|
||||||
&arg.ty,
|
&arg.ty,
|
||||||
&value,
|
&value,
|
||||||
QueryPathNode {
|
QueryPathNode {
|
||||||
|
@ -525,19 +524,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn string_into_enum() {
|
// fn string_into_enum() {
|
||||||
expect_fails_rule!(
|
// expect_fails_rule!(
|
||||||
factory,
|
// factory,
|
||||||
r#"
|
// r#"
|
||||||
{
|
// {
|
||||||
dog {
|
// dog {
|
||||||
doesKnowCommand(dogCommand: "SIT")
|
// doesKnowCommand(dogCommand: "SIT")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
"#,
|
// "#,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn boolean_into_enum() {
|
fn boolean_into_enum() {
|
||||||
|
|
|
@ -20,7 +20,6 @@ impl<'a> Visitor<'a> for DefaultValuesOfCorrectType {
|
||||||
));
|
));
|
||||||
} else if let Some(reason) = is_valid_input_value(
|
} else if let Some(reason) = is_valid_input_value(
|
||||||
ctx.registry,
|
ctx.registry,
|
||||||
ctx.variables,
|
|
||||||
&variable_definition.node.var_type.to_string(),
|
&variable_definition.node.var_type.to_string(),
|
||||||
&value.node,
|
&value.node,
|
||||||
QueryPathNode {
|
QueryPathNode {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::context::QueryPathNode;
|
use crate::context::QueryPathNode;
|
||||||
use crate::parser::types::{ConstValue, Value};
|
use crate::parser::types::{ConstValue, Value};
|
||||||
use crate::{registry, QueryPathSegment, Variables};
|
use crate::{registry, QueryPathSegment};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[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(
|
pub fn is_valid_input_value(
|
||||||
registry: ®istry::Registry,
|
registry: ®istry::Registry,
|
||||||
variables: Option<&Variables>,
|
|
||||||
type_name: &str,
|
type_name: &str,
|
||||||
value: &ConstValue,
|
value: &ConstValue,
|
||||||
path_node: QueryPathNode,
|
path_node: QueryPathNode,
|
||||||
|
@ -47,13 +46,12 @@ pub fn is_valid_input_value(
|
||||||
&path_node,
|
&path_node,
|
||||||
format!("expected type \"{}\"", type_name),
|
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 {
|
registry::MetaTypeName::List(type_name) => match value {
|
||||||
ConstValue::List(elems) => elems.iter().enumerate().find_map(|(idx, elem)| {
|
ConstValue::List(elems) => elems.iter().enumerate().find_map(|(idx, elem)| {
|
||||||
is_valid_input_value(
|
is_valid_input_value(
|
||||||
registry,
|
registry,
|
||||||
variables,
|
|
||||||
type_name,
|
type_name,
|
||||||
elem,
|
elem,
|
||||||
QueryPathNode {
|
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) => {
|
registry::MetaTypeName::Named(type_name) => {
|
||||||
if let ConstValue::Null = value {
|
if let ConstValue::Null = value {
|
||||||
|
@ -98,6 +96,19 @@ pub fn is_valid_input_value(
|
||||||
None
|
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(
|
_ => Some(valid_error(
|
||||||
&path_node,
|
&path_node,
|
||||||
format!("expected type \"{}\"", type_name),
|
format!("expected type \"{}\"", type_name),
|
||||||
|
@ -131,7 +142,6 @@ pub fn is_valid_input_value(
|
||||||
|
|
||||||
if let Some(reason) = is_valid_input_value(
|
if let Some(reason) = is_valid_input_value(
|
||||||
registry,
|
registry,
|
||||||
variables,
|
|
||||||
&field.ty,
|
&field.ty,
|
||||||
value,
|
value,
|
||||||
QueryPathNode {
|
QueryPathNode {
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user