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]
|
||||
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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -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"
|
||||
|
|
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
|
||||
|
||||
#![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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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]
|
||||
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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
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
|
||||
|
||||
#![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};
|
||||
|
|
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]
|
||||
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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -320,6 +320,7 @@ where
|
|||
self.0
|
||||
.extensions
|
||||
.iter()
|
||||
.chain(request.extensions.iter())
|
||||
.map(|factory| factory())
|
||||
.collect_vec(),
|
||||
));
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::context::QueryPathNode;
|
||||
use crate::parser::types::{ConstValue, Value};
|
||||
use crate::{registry, QueryPathSegment, Variables};
|
||||
use crate::{registry, QueryPathSegment};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
|
@ -36,7 +36,6 @@ fn referenced_variables_to_vec<'a>(value: &'a Value, vars: &mut Vec<&'a str>) {
|
|||
|
||||
pub fn is_valid_input_value(
|
||||
registry: ®istry::Registry,
|
||||
variables: Option<&Variables>,
|
||||
type_name: &str,
|
||||
value: &ConstValue,
|
||||
path_node: QueryPathNode,
|
||||
|
@ -47,13 +46,12 @@ pub fn is_valid_input_value(
|
|||
&path_node,
|
||||
format!("expected type \"{}\"", type_name),
|
||||
)),
|
||||
_ => is_valid_input_value(registry, variables, type_name, value, path_node),
|
||||
_ => is_valid_input_value(registry, type_name, value, path_node),
|
||||
},
|
||||
registry::MetaTypeName::List(type_name) => match value {
|
||||
ConstValue::List(elems) => elems.iter().enumerate().find_map(|(idx, elem)| {
|
||||
is_valid_input_value(
|
||||
registry,
|
||||
variables,
|
||||
type_name,
|
||||
elem,
|
||||
QueryPathNode {
|
||||
|
@ -62,7 +60,7 @@ pub fn is_valid_input_value(
|
|||
},
|
||||
)
|
||||
}),
|
||||
_ => is_valid_input_value(registry, variables, type_name, value, path_node),
|
||||
_ => is_valid_input_value(registry, type_name, value, path_node),
|
||||
},
|
||||
registry::MetaTypeName::Named(type_name) => {
|
||||
if let ConstValue::Null = value {
|
||||
|
@ -98,6 +96,19 @@ pub fn is_valid_input_value(
|
|||
None
|
||||
}
|
||||
}
|
||||
ConstValue::String(name) => {
|
||||
if !enum_values.contains_key(name.as_str()) {
|
||||
Some(valid_error(
|
||||
&path_node,
|
||||
format!(
|
||||
"enumeration type \"{}\" does not contain the value \"{}\"",
|
||||
enum_name, name
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => Some(valid_error(
|
||||
&path_node,
|
||||
format!("expected type \"{}\"", type_name),
|
||||
|
@ -131,7 +142,6 @@ pub fn is_valid_input_value(
|
|||
|
||||
if let Some(reason) = is_valid_input_value(
|
||||
registry,
|
||||
variables,
|
||||
&field.ty,
|
||||
value,
|
||||
QueryPathNode {
|
||||
|
|
|
@ -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