This commit is contained in:
sunli 2020-04-14 09:53:17 +08:00
parent a53e61abf9
commit 09d5e5f489
38 changed files with 518 additions and 6383 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql"
version = "1.8.1"
version = "1.9.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "The GraphQL server library implemented by rust"
@ -18,7 +18,7 @@ default = ["bson", "uuid", "url", "chrono-tz", "validators"]
validators = ["regex"]
[dependencies]
async-graphql-derive = { path = "async-graphql-derive", version = "1.8.1" }
async-graphql-derive = { path = "async-graphql-derive", version = "1.9.0" }
graphql-parser = "=0.2.3"
anyhow = "1.0.26"
thiserror = "1.0.11"
@ -48,12 +48,12 @@ chrono-tz = { version = "0.5.1", optional = true }
[dev-dependencies]
async-std = { version = "1.5.0", features = ["attributes"] }
actix-rt = "1.0.0"
tide = "0.6.0"
actix-web = "2.0.0"
serde = "1.0.104"
serde_derive = "1.0.104"
[workspace]
members = [
"async-graphql-derive",
"async-graphql-actix-web"
"async-graphql-actix-web",
"async-graphql-warp",
]

View File

@ -31,13 +31,9 @@ It supports all of the GraphQL specifications and is easy to integrate into exis
* [Cargo package](https://crates.io/crates/async-graphql)
* Minimum supported Rust version: 1.42 or later
## Example
## Examples
```shell script
cargo run --example actix_web
```
Open `http://localhost:8000` in browser
https://github.com/sunli829/async-graphql-examples
## Benchmark

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-actix-web"
version = "0.8.1"
version = "1.0.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "async-graphql for actix-web"
@ -13,19 +13,11 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "..", version = "1.8.1" }
async-graphql = { path = "..", version = "1.9.0" }
actix-web = "2.0.0"
actix-multipart = "0.2.0"
actix-web-actors = "2.0.0"
actix = "0.9.0"
actix-rt = "1.0.0"
futures = "0.3.0"
mime = "0.3.16"
bytes = "0.5.4"
serde = "1.0.104"
serde_derive = "1.0.104"
serde_json = "1.0.48"
slab = "0.4.2"
actix_derive = "0.5.0"
[dev-dependencies]
actix-rt = "1.0.0"

View File

@ -1,143 +0,0 @@
use actix::clock::Duration;
use actix_web::{web, App, HttpServer};
use async_graphql::{Context, FieldResult, Schema, SimpleBroker, ID};
use futures::lock::Mutex;
use futures::{Stream, StreamExt};
use slab::Slab;
use std::sync::Arc;
#[derive(Clone)]
struct Book {
id: ID,
name: String,
author: String,
}
#[async_graphql::Object]
impl Book {
#[field]
async fn id(&self) -> &str {
&self.id
}
#[field]
async fn name(&self) -> &str {
&self.name
}
#[field]
async fn author(&self) -> &str {
&self.author
}
}
type Storage = Arc<Mutex<Slab<Book>>>;
struct QueryRoot;
#[async_graphql::Object]
impl QueryRoot {
#[field]
async fn books(&self, ctx: &Context<'_>) -> Vec<Book> {
let books = ctx.data::<Storage>().lock().await;
books.iter().map(|(_, book)| book).cloned().collect()
}
}
struct MutationRoot;
#[async_graphql::Object]
impl MutationRoot {
#[field]
async fn create_book(&self, ctx: &Context<'_>, name: String, author: String) -> ID {
let mut books = ctx.data::<Storage>().lock().await;
let entry = books.vacant_entry();
let id: ID = entry.key().into();
let book = Book {
id: id.clone(),
name,
author,
};
entry.insert(book);
SimpleBroker::publish(BookChanged {
mutation_type: MutationType::Created,
id: id.clone(),
});
id
}
#[field]
async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> FieldResult<bool> {
let mut books = ctx.data::<Storage>().lock().await;
let id = id.parse::<usize>()?;
if books.contains(id) {
books.remove(id);
SimpleBroker::publish(BookChanged {
mutation_type: MutationType::Deleted,
id: id.into(),
});
Ok(true)
} else {
Ok(false)
}
}
}
#[async_graphql::Enum]
enum MutationType {
Created,
Deleted,
}
#[async_graphql::SimpleObject]
#[derive(Clone)]
struct BookChanged {
#[field]
mutation_type: MutationType,
#[field]
id: ID,
}
struct SubscriptionRoot;
#[async_graphql::Subscription]
impl SubscriptionRoot {
#[field]
async fn interval(&self, n: i32) -> impl Stream<Item = i32> {
let mut value = 0;
actix_rt::time::interval(Duration::from_secs(1)).map(move |_| {
value += n;
value
})
}
#[field]
async fn books(&self, mutation_type: Option<MutationType>) -> impl Stream<Item = BookChanged> {
SimpleBroker::<BookChanged>::subscribe().filter(move |event| {
let res = if let Some(mutation_type) = mutation_type {
event.mutation_type == mutation_type
} else {
true
};
futures::future::ready(res)
})
}
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
.data(Storage::default())
.finish();
let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
.enable_ui("http://localhost:8000", Some("ws://localhost:8000"))
.enable_subscription()
.build();
App::new().service(web::resource("/").to(handler))
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -1,36 +0,0 @@
use actix_web::{web, App, HttpServer};
use async_graphql::{Context, EmptyMutation, EmptySubscription, Schema};
struct MyToken(Option<String>);
struct QueryRoot;
#[async_graphql::Object]
impl QueryRoot {
#[field]
async fn current_token<'a>(&self, ctx: &'a Context<'_>) -> Option<&'a str> {
ctx.data::<MyToken>().0.as_deref()
}
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
let handler = async_graphql_actix_web::HandlerBuilder::new(schema)
.enable_subscription()
.enable_ui("http://localhost:8000", None)
.on_request(|req, builder| {
builder.data(MyToken(
req.headers()
.get("Token")
.and_then(|value| value.to_str().map(ToString::to_string).ok()),
))
})
.build();
App::new().service(web::resource("/").to(handler))
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -1,88 +0,0 @@
use actix_web::{web, App, HttpServer};
use async_graphql::{Context, EmptySubscription, Schema, Upload, ID};
use futures::lock::Mutex;
use slab::Slab;
#[async_graphql::SimpleObject]
#[derive(Clone)]
struct FileInfo {
#[field]
id: ID,
#[field]
filename: String,
#[field]
mimetype: Option<String>,
#[field]
path: String,
}
type Storage = Mutex<Slab<FileInfo>>;
struct QueryRoot;
#[async_graphql::Object]
impl QueryRoot {
#[field]
async fn uploads(&self, ctx: &Context<'_>) -> Vec<FileInfo> {
let storage = ctx.data::<Storage>().lock().await;
storage.iter().map(|(_, file)| file).cloned().collect()
}
}
struct MutationRoot;
#[async_graphql::Object]
impl MutationRoot {
#[field]
async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo {
let mut storage = ctx.data::<Storage>().lock().await;
println!("files count: {}", storage.len());
let entry = storage.vacant_entry();
let info = FileInfo {
id: entry.key().into(),
filename: file.filename,
mimetype: file.content_type,
path: file.path.display().to_string(),
};
entry.insert(info.clone());
info
}
#[field]
async fn multiple_upload(&self, ctx: &Context<'_>, files: Vec<Upload>) -> Vec<FileInfo> {
let mut infos = Vec::new();
let mut storage = ctx.data::<Storage>().lock().await;
for file in files {
let entry = storage.vacant_entry();
let info = FileInfo {
id: entry.key().into(),
filename: file.filename,
mimetype: file.content_type,
path: file.path.display().to_string(),
};
entry.insert(info.clone());
infos.push(info)
}
infos
}
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(Storage::default())
.finish();
HttpServer::new(move || {
let handler = async_graphql_actix_web::HandlerBuilder::new(schema.clone())
.enable_subscription()
.build();
App::new().service(web::resource("/").to(handler))
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -1,166 +1,60 @@
mod request;
mod session;
//! Async-graphql integration with Actix-web
use crate::request::RequestWrapper;
use crate::session::WsSession;
use actix_web::http::{header, Method};
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use actix_web_actors::ws;
use async_graphql::http::{playground_source, GQLHttpRequest, GQLResponse, IntoQueryBuilder};
use async_graphql::{ObjectType, QueryBuilder, Schema, SubscriptionType};
use futures::Future;
#![warn(missing_docs)]
mod subscription;
use actix_web::dev::{Payload, PayloadStream};
use actix_web::{http, web, Error, FromRequest, HttpRequest};
use async_graphql::http::StreamBody;
use async_graphql::{IntoQueryBuilder, QueryBuilder};
use futures::channel::mpsc;
use futures::{Future, SinkExt, StreamExt, TryFutureExt};
use std::pin::Pin;
use std::sync::Arc;
type BoxOnRequestFn<Query, Mutation, Subscription> = Arc<
dyn for<'a> Fn(
&HttpRequest,
QueryBuilder<Query, Mutation, Subscription>,
) -> QueryBuilder<Query, Mutation, Subscription>,
>;
pub use subscription::WSSubscription;
pub struct HandlerBuilder<Query, Mutation, Subscription> {
schema: Schema<Query, Mutation, Subscription>,
enable_subscription: bool,
enable_ui: Option<(String, Option<String>)>,
on_request: Option<BoxOnRequestFn<Query, Mutation, Subscription>>,
}
/// Extractor for GraphQL request
///
/// It's a wrapper of `QueryBuilder`, you can use `GQLRequest::into_inner` unwrap it to `QueryBuilder`.
pub struct GQLRequest(QueryBuilder);
impl<Query, Mutation, Subscription> HandlerBuilder<Query, Mutation, Subscription>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
/// Create an HTTP handler builder
pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
Self {
schema,
enable_subscription: false,
enable_ui: None,
on_request: None,
}
}
/// Enable GraphQL playground
///
/// 'endpoint' is the endpoint of the GraphQL Request.
/// 'subscription_endpoint' is the endpoint of the GraphQL Subscription.
pub fn enable_ui(self, endpoint: &str, subscription_endpoint: Option<&str>) -> Self {
Self {
enable_ui: Some((
endpoint.to_string(),
subscription_endpoint.map(|s| s.to_string()),
)),
..self
}
}
/// Enable GraphQL Subscription.
pub fn enable_subscription(self) -> Self {
Self {
enable_subscription: true,
..self
}
}
/// When a new request arrives, you can use this closure to append your own data to the `QueryBuilder`.
pub fn on_request<
F: for<'a> Fn(
&HttpRequest,
QueryBuilder<Query, Mutation, Subscription>,
) -> QueryBuilder<Query, Mutation, Subscription>
+ 'static,
>(
self,
f: F,
) -> Self {
Self {
on_request: Some(Arc::new(f)),
..self
}
}
/// Create an HTTP handler.
pub fn build(
self,
) -> impl Fn(
HttpRequest,
web::Payload,
) -> Pin<Box<dyn Future<Output = actix_web::Result<HttpResponse>>>>
+ Clone
+ 'static {
let schema = self.schema.clone();
let enable_ui = self.enable_ui;
let enable_subscription = self.enable_subscription;
let on_request = self.on_request;
move |req: HttpRequest, payload: web::Payload| {
let schema = schema.clone();
let enable_ui = enable_ui.clone();
let on_request = on_request.clone();
Box::pin(async move {
if req.method() == Method::GET {
if enable_subscription {
if let Some(s) = req.headers().get(header::UPGRADE) {
if let Ok(s) = s.to_str() {
if s.to_ascii_lowercase().contains("websocket") {
return ws::start_with_protocols(
WsSession::new(schema.clone()),
&["graphql-ws"],
&req,
payload,
);
}
}
}
}
if let Some((endpoint, subscription_endpoint)) = &enable_ui {
return Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(
endpoint,
subscription_endpoint.as_deref(),
)));
}
}
if req.method() == Method::POST {
let content_type = req
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(|s| s.to_string());
let r = RequestWrapper(content_type, payload);
r.content_type();
let mut builder = match IntoQueryBuilder::into_query_builder(r, &schema).await {
Ok(builder) => builder,
Err(err) => {
return Ok(web::Json(GQLResponse(Err(err))).respond_to(&req).await?)
}
};
let cache_control = builder.cache_control().value();
if let Some(on_request) = &on_request {
builder = on_request(&req, builder);
}
let mut resp = web::Json(GQLResponse(builder.execute().await))
.respond_to(&req)
.await?;
if let Some(cache_control) = cache_control {
resp.headers_mut().insert(
header::CACHE_CONTROL,
header::HeaderValue::from_str(&cache_control).unwrap(),
);
}
Ok(resp)
} else {
Ok(HttpResponse::MethodNotAllowed().finish())
}
})
}
impl GQLRequest {
/// Unwrap it to `QueryBuilder`.
pub fn into_inner(self) -> QueryBuilder {
self.0
}
}
impl FromRequest for GQLRequest {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<GQLRequest, Error>>>>;
type Config = ();
fn from_request(req: &HttpRequest, payload: &mut Payload<PayloadStream>) -> Self::Future {
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(
(content_type, StreamBody::new(rx))
.into_query_builder()
.map_ok(GQLRequest)
.map_err(actix_web::error::ErrorBadRequest),
)
}
}

View File

@ -11,21 +11,23 @@ use std::time::{Duration, Instant};
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct WsSession<Query, Mutation, Subscription> {
/// Actor for subscription via websocket
pub struct WSSubscription<Query, Mutation, Subscription> {
schema: Schema<Query, Mutation, Subscription>,
hb: Instant,
sink: Option<mpsc::Sender<Bytes>>,
}
impl<Query, Mutation, Subscription> WsSession<Query, Mutation, Subscription>
impl<Query, Mutation, Subscription> WSSubscription<Query, Mutation, Subscription>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
pub fn new(schema: Schema<Query, Mutation, Subscription>) -> Self {
/// Create an actor for subscription connection via websocket.
pub fn new(schema: &Schema<Query, Mutation, Subscription>) -> Self {
Self {
schema,
schema: schema.clone(),
hb: Instant::now(),
sink: None,
}
@ -41,7 +43,7 @@ where
}
}
impl<Query, Mutation, Subscription> Actor for WsSession<Query, Mutation, Subscription>
impl<Query, Mutation, Subscription> Actor for WSSubscription<Query, Mutation, Subscription>
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,
@ -59,7 +61,7 @@ where
}
impl<Query, Mutation, Subscription> StreamHandler<Result<Message, ProtocolError>>
for WsSession<Query, Mutation, Subscription>
for WSSubscription<Query, Mutation, Subscription>
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,
@ -99,7 +101,7 @@ where
}
impl<Query, Mutation, Subscription> StreamHandler<Bytes>
for WsSession<Query, Mutation, Subscription>
for WSSubscription<Query, Mutation, Subscription>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,

View File

@ -1,6 +1,6 @@
[package]
name = "async-graphql-derive"
version = "1.8.1"
version = "1.9.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
description = "Macros for async-graphql"

View File

@ -427,7 +427,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
}.into_error(field.position))
}
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, pos: #crate_name::Pos, params: &#crate_name::Value) -> #crate_name::Result<serde_json::Value> {
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, pos: #crate_name::Pos, params: &#crate_name::Value) -> #crate_name::Result<#crate_name::serde_json::Value> {
let params = match params {
#crate_name::Value::Object(params) => params,
_ => return Err(#crate_name::QueryError::EntityNotFound.into_error(pos)),

View File

@ -0,0 +1,11 @@
[package]
name = "async-graphql-warp"
version = "1.0.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
[dependencies]
async-graphql = { path = "..", version = "1.9.0" }
warp = "0.2.2"
futures = "0.3.0"
bytes = "0.5.4"

View File

@ -0,0 +1,169 @@
//! Async-graphql integration with Wrap
#![warn(missing_docs)]
#![allow(clippy::type_complexity)]
use async_graphql::http::StreamBody;
use async_graphql::{
IntoQueryBuilder, ObjectType, QueryBuilder, Schema, SubscriptionType, WebSocketTransport,
};
use bytes::Bytes;
use futures::select;
use futures::{SinkExt, StreamExt};
use warp::filters::ws::Message;
use warp::filters::BoxedFilter;
use warp::reject::Reject;
use warp::{Filter, Rejection, Reply};
/// Bad request error
///
/// It's a wrapper of `async_graphql::ParseRequestError`.
pub struct BadRequest(pub async_graphql::ParseRequestError);
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 `Schema` and `QuertBuilder`.
///
/// # Examples
///
/// ```no_run
///
/// use async_graphql::*;
/// use warp::Filter;
/// use std::convert::Infallible;
///
/// struct QueryRoot;
///
/// #[Object]
/// impl QueryRoot {
/// #[field]
/// async fn value(&self, ctx: &Context<'_>) -> i32 {
/// unimplemented!()
/// }
/// }
///
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
/// let filter = async_graphql_warp::graphql(schema).and_then(|schema, builder| async move {
/// let resp = builder.execute(&schema).await;
/// Ok::<_, Infallible>(warp::reply::json(&GQLResponse(resp)).into_response())
/// });
/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
/// ```
pub fn graphql<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> BoxedFilter<((Schema<Query, Mutation, Subscription>, QueryBuilder),)>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
warp::any()
.and(warp::post())
.and(warp::header::optional::<String>("content-type"))
.and(warp::body::stream())
.and(warp::any().map(move || schema.clone()))
.and_then(|content_type, body, schema| async move {
let builder = (content_type, StreamBody::new(body))
.into_query_builder()
.await
.map_err(|err| warp::reject::custom(BadRequest(err)))?;
Ok::<_, Rejection>((schema, builder))
})
.boxed()
}
/// GraphQL subscription filter
///
/// # Examples
///
/// ```no_run
/// use async_graphql::*;
/// use warp::Filter;
/// use futures::{Stream, StreamExt};
/// use tokio::time::Duration;
///
/// struct QueryRoot;
///
/// #[Object]
/// impl QueryRoot {}
///
/// struct SubscriptionRoot;
///
/// #[Subscription]
/// impl SubscriptionRoot {
/// #[field]
/// async fn tick(&self) -> impl Stream<String> {
/// tokio::time::interval(Duration::from_secs(1)).map(|n| format!("{}", n.elapsed().as_secs_f32()))
/// }
/// }
///
/// let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot);
/// let filter = async_graphql_warp::graphql_subscription(schema);
/// warp::serve(filter).run(([0, 0, 0, 0], 8000)).await;
/// ```
pub fn graphql_subscription<Query, Mutation, Subscription>(
schema: Schema<Query, Mutation, Subscription>,
) -> BoxedFilter<(impl Reply,)>
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
warp::any()
.and(warp::ws())
.and(warp::any().map(move || schema.clone()))
.map(
|ws: warp::ws::Ws, schema: Schema<Query, Mutation, Subscription>| {
ws.on_upgrade(move |websocket| {
let (mut tx, rx) = websocket.split();
let (mut stx, srx) =
schema.subscription_connection(WebSocketTransport::default());
let mut rx = rx.fuse();
let mut srx = srx.fuse();
async move {
loop {
select! {
bytes = srx.next() => {
if let Some(bytes) = bytes {
if tx
.send(Message::text(unsafe {
String::from_utf8_unchecked(bytes.to_vec())
}))
.await
.is_err()
{
return;
}
} else {
return;
}
}
msg = rx.next() => {
if let Some(Ok(msg)) = msg {
if msg.is_text() {
if stx.send(Bytes::copy_from_slice(msg.as_bytes())).await.is_err() {
return;
}
}
}
}
}
}
}
})
},
).map(|reply| {
warp::reply::with_header(reply, "Sec-WebSocket-Protocol", "graphql-ws")
})
.boxed()
}

View File

@ -1,54 +0,0 @@
mod starwars;
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
};
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
use futures::TryFutureExt;
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
async fn index(s: web::Data<StarWarsSchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
req.into_inner()
.into_query_builder(&s)
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
async fn gql_graphiql() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(graphiql_source("/"))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.data(
Schema::build(starwars::QueryRoot, EmptyMutation, EmptySubscription)
.data(starwars::StarWars::new())
.extension(|| async_graphql::extensions::ApolloTracing::default())
.finish(),
)
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
.service(
web::resource("/graphiql")
.guard(guard::Get())
.to(gql_graphiql),
)
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -1,153 +0,0 @@
#[macro_use]
extern crate thiserror;
use actix_rt;
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
};
use async_graphql::ErrorExtensions;
use async_graphql::*;
use futures::TryFutureExt;
use serde_json::json;
#[derive(Debug, Error)]
pub enum MyError {
#[error("Could not find resource")]
NotFound,
#[error("ServerError")]
ServerError(String),
#[error("No Extensions")]
ErrorWithoutExtensions,
}
impl ErrorExtensions for MyError {
// lets define our base extensions
fn extend(&self) -> FieldError {
let extensions = match self {
MyError::NotFound => json!({"code": "NOT_FOUND"}),
MyError::ServerError(reason) => json!({ "reason": reason }),
MyError::ErrorWithoutExtensions => {
json!("This will be ignored since it does not represent an object.")
}
};
FieldError(format!("{}", self), Some(extensions))
}
}
struct QueryRoot;
#[Object]
impl QueryRoot {
// It works on foreign types without extensions as before
#[field]
async fn parse_without_extensions(&self) -> FieldResult<i32> {
Ok("234a".parse()?)
}
// Foreign types can be extended
#[field]
async fn parse_with_extensions(&self) -> FieldResult<i32> {
Ok("234a"
.parse()
.map_err(|e: std::num::ParseIntError| e.extend_with(|_| json!({"code": 404})))?)
}
// THIS does unfortunately NOT work because ErrorExtensions is implemented for &E and not E.
// Which is necessary for the overwrite by the user.
//#[field]
// async fn parse_with_extensions_result(&self) -> FieldResult<i32> {
// Ok("234a".parse().extend_err(|_| json!({"code": 404}))?)
// }
// Using our own types we can implement some base extensions
#[field]
async fn extend(&self) -> FieldResult<i32> {
Err(MyError::NotFound.extend())?
}
// Or on the result
#[field]
async fn extend_result(&self) -> FieldResult<i32> {
Err(MyError::NotFound).extend()?
}
// Base extensions can be further extended
#[field]
async fn more_extensions(&self) -> FieldResult<String> {
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
Err(MyError::NotFound.extend_with(|_e| json!({"reason": "my reason"})))?
}
// works with results as well
#[field]
async fn more_extensions_on_result(&self) -> FieldResult<String> {
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
Err(MyError::NotFound).extend_err(|_e| json!({"reason": "my reason"}))?
}
// extend_with is chainable
#[field]
async fn chainable_extensions(&self) -> FieldResult<String> {
let err = MyError::NotFound
.extend_with(|_| json!({"ext1": 1}))
.extend_with(|_| json!({"ext2": 2}))
.extend_with(|_| json!({"ext3": 3}));
Err(err)?
}
// extend_with overwrites keys which are already present
#[field]
async fn overwrite(&self) -> FieldResult<String> {
Err(MyError::NotFound.extend_with(|_| json!({"code": "overwritten"})))?
}
}
async fn index(
s: web::Data<Schema<QueryRoot, EmptyMutation, EmptySubscription>>,
req: web::Json<GQLRequest>,
) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
req.into_inner()
.into_query_builder(&s)
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
async fn gql_graphiql() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(graphiql_source("/"))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
println!("Playground: http://localhost:8000");
println!("Graphiql: http://localhost:8000/graphiql");
HttpServer::new(move || {
App::new()
.data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription))
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
.service(
web::resource("/graphiql")
.guard(guard::Get())
.to(gql_graphiql),
)
})
.bind("127.0.0.1:8000")?
.run()
.await
}

View File

@ -1,68 +0,0 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
#[SimpleObject]
struct User {
#[field]
id: ID,
#[field]
username: String,
}
struct Query;
#[Object(extends)]
impl Query {
#[field]
async fn me(&self) -> User {
User {
id: "1234".into(),
username: "Me".to_string(),
}
}
#[entity]
async fn find_user_by_id(&self, id: ID) -> User {
let username = if id == "1234" {
"Me".to_string()
} else {
format!("User {}", id)
};
User { id, username }
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
req.into_inner()
.into_query_builder(&s)
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4001")?
.run()
.await
}

View File

@ -1,88 +0,0 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
#[SimpleObject]
struct Product {
#[field]
upc: String,
#[field]
name: String,
#[field]
price: i32,
}
struct Query;
#[Object(extends)]
impl Query {
#[field]
async fn top_products<'a>(&self, ctx: &'a Context<'_>) -> &'a Vec<Product> {
ctx.data::<Vec<Product>>()
}
#[entity]
async fn find_product_by_upc<'a>(
&self,
ctx: &'a Context<'_>,
upc: String,
) -> Option<&'a Product> {
let hats = ctx.data::<Vec<Product>>();
hats.iter().find(|product| product.upc == upc)
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
req.into_inner()
.into_query_builder(&s)
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let hats = vec![
Product {
upc: "top-1".to_string(),
name: "Trilby".to_string(),
price: 11,
},
Product {
upc: "top-2".to_string(),
name: "Fedora".to_string(),
price: 22,
},
Product {
upc: "top-3".to_string(),
name: "Boater".to_string(),
price: 33,
},
];
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.data(hats)
.finish();
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4002")?
.run()
.await
}

View File

@ -1,131 +0,0 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse, IntoQueryBuilder};
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
struct User {
id: ID,
}
#[Object(extends)]
impl User {
#[field(external)]
async fn id(&self) -> &ID {
&self.id
}
#[field]
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
let reviews = ctx.data::<Vec<Review>>();
reviews
.iter()
.filter(|review| review.author.id == self.id)
.collect()
}
}
struct Product {
upc: String,
}
#[Object(extends)]
impl Product {
#[field(external)]
async fn upc(&self) -> &String {
&self.upc
}
#[field]
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
let reviews = ctx.data::<Vec<Review>>();
reviews
.iter()
.filter(|review| review.product.upc == self.upc)
.collect()
}
}
#[SimpleObject]
struct Review {
#[field]
body: String,
#[field(provides = "username")]
author: User,
#[field]
product: Product,
}
struct Query;
#[Object]
impl Query {
#[entity]
async fn find_user_by_id<'a>(&self, id: ID) -> User {
User { id }
}
#[entity]
async fn find_product_by_upc<'a>(&self, upc: String) -> Product {
Product { upc }
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
req.into_inner()
.into_query_builder(&s)
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let reviews = vec![
Review {
body: "A highly effective form of birth control.".into(),
author: User { id: "1234".into() },
product: Product {
upc: "top-1".to_string(),
},
},
Review {
body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(),
author: User { id: "1234".into() },
product: Product {
upc: "top-1".to_string(),
},
},
Review {
body: "This is the last straw. Hat you will wear. 11/10".into(),
author: User { id: "7777".into() },
product: Product {
upc: "top-1".to_string(),
},
},
];
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.data(reviews)
.finish();
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4003")?
.run()
.await
}

View File

@ -1,19 +0,0 @@
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require("@apollo/gateway");
const gateway = new ApolloGateway({
serviceList: [
{ name: 'accounts', url: 'http://localhost:4001' },
{ name: 'products', url: 'http://localhost:4002' },
{ name: 'reviews', url: 'http://localhost:4003' }
],
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

View File

@ -1,10 +0,0 @@
module.exports = {
testEnvironment: "node",
testMatch: ["<rootDir>/**/*-test.js"],
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
moduleFileExtensions: ["js"],
modulePaths: ["<rootDir>/node_modules"],
// transform: {
// '^.+\\.jsx?$': 'babel-jest',
// },
};

View File

@ -1,17 +0,0 @@
{
"name": "gateway",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@apollo/gateway": "^0.11.7",
"apollo-server": "^2.9.16",
"graphql": "^14.6.0"
},
"devDependencies": {
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
"apollo-link-http": "^1.5.16",
"jest": "^25.1.0",
"node-fetch": "^2.6.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
#!/bin/bash
function cleanup {
kill "$ACCOUNTS_PID"
kill "$PRODUCTS_PID"
kill "$REVIEWS_PID"
}
trap cleanup EXIT
cargo build --example federation-accounts
cargo build --example federation-products
cargo build --example federation-reviews
cargo run --example federation-accounts &
ACCOUNTS_PID=$!
cargo run --example federation-products &
PRODUCTS_PID=$!
cargo run --example federation-reviews &
REVIEWS_PID=$!
sleep 3
node federation/index.js

View File

@ -1,135 +0,0 @@
mod model;
use model::Episode;
pub use model::QueryRoot;
use slab::Slab;
use std::collections::HashMap;
pub struct StarWarsChar {
id: &'static str,
name: &'static str,
friends: Vec<usize>,
appears_in: Vec<Episode>,
home_planet: Option<&'static str>,
primary_function: Option<&'static str>,
}
pub struct StarWars {
luke: usize,
artoo: usize,
chars: Slab<StarWarsChar>,
human_data: HashMap<&'static str, usize>,
droid_data: HashMap<&'static str, usize>,
}
impl StarWars {
pub fn new() -> Self {
let mut chars = Slab::new();
let luke = chars.insert(StarWarsChar {
id: "1000",
name: "Luke Skywalker",
friends: vec![],
appears_in: vec![],
home_planet: Some("Tatooine"),
primary_function: None,
});
let vader = chars.insert(StarWarsChar {
id: "1001",
name: "Luke Skywalker",
friends: vec![],
appears_in: vec![],
home_planet: Some("Tatooine"),
primary_function: None,
});
let han = chars.insert(StarWarsChar {
id: "1002",
name: "Han Solo",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: None,
});
let leia = chars.insert(StarWarsChar {
id: "1003",
name: "Leia Organa",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: Some("Alderaa"),
primary_function: None,
});
let tarkin = chars.insert(StarWarsChar {
id: "1004",
name: "Wilhuff Tarkin",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: None,
});
let threepio = chars.insert(StarWarsChar {
id: "2000",
name: "C-3PO",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: Some("Protocol"),
});
let artoo = chars.insert(StarWarsChar {
id: "2001",
name: "R2-D2",
friends: vec![],
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
home_planet: None,
primary_function: Some("Astromech"),
});
chars[luke].friends = vec![han, leia, threepio, artoo];
chars[vader].friends = vec![tarkin];
chars[han].friends = vec![luke, leia, artoo];
chars[leia].friends = vec![luke, han, threepio, artoo];
chars[tarkin].friends = vec![vader];
chars[threepio].friends = vec![luke, han, leia, artoo];
chars[artoo].friends = vec![luke, han, leia];
let mut human_data = HashMap::new();
human_data.insert("1000", luke);
human_data.insert("1001", vader);
human_data.insert("1002", han);
human_data.insert("1003", leia);
human_data.insert("1004", tarkin);
let mut droid_data = HashMap::new();
droid_data.insert("2000", threepio);
droid_data.insert("2001", artoo);
Self {
luke,
artoo,
chars,
human_data,
droid_data,
}
}
pub fn human(&self, id: &str) -> Option<usize> {
self.human_data.get(id).cloned()
}
pub fn droid(&self, id: &str) -> Option<usize> {
self.droid_data.get(id).cloned()
}
pub fn humans(&self) -> Vec<usize> {
self.human_data.values().cloned().collect()
}
pub fn droids(&self) -> Vec<usize> {
self.droid_data.values().cloned().collect()
}
}

View File

@ -1,173 +0,0 @@
use super::StarWars;
use async_graphql::{Connection, Context, DataSource, EmptyEdgeFields, FieldResult};
#[async_graphql::Enum(desc = "One of the films in the Star Wars Trilogy")]
pub enum Episode {
#[item(desc = "Released in 1977.")]
NewHope,
#[item(desc = "Released in 1980.")]
Empire,
#[item(desc = "Released in 1983.")]
Jedi,
}
pub struct Human(usize);
#[async_graphql::Object(desc = "A humanoid creature in the Star Wars universe.")]
impl Human {
#[field(desc = "The id of the human.")]
async fn id(&self, ctx: &Context<'_>) -> &str {
ctx.data::<StarWars>().chars[self.0].id
}
#[field(desc = "The name of the human.")]
async fn name(&self, ctx: &Context<'_>) -> &str {
ctx.data::<StarWars>().chars[self.0].name
}
#[field(desc = "The friends of the human, or an empty list if they have none.")]
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
ctx.data::<StarWars>().chars[self.0]
.friends
.iter()
.map(|id| Human(*id).into())
.collect()
}
#[field(desc = "Which movies they appear in.")]
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
&ctx.data::<StarWars>().chars[self.0].appears_in
}
#[field(desc = "The home planet of the human, or null if unknown.")]
async fn home_planet<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
&ctx.data::<StarWars>().chars[self.0].home_planet
}
}
pub struct Droid(usize);
#[async_graphql::Object(desc = "A mechanical creature in the Star Wars universe.")]
impl Droid {
#[field(desc = "The id of the droid.")]
async fn id(&self, ctx: &Context<'_>) -> &str {
ctx.data::<StarWars>().chars[self.0].id
}
#[field(desc = "The name of the droid.")]
async fn name(&self, ctx: &Context<'_>) -> &str {
ctx.data::<StarWars>().chars[self.0].name
}
#[field(desc = "The friends of the droid, or an empty list if they have none.")]
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
ctx.data::<StarWars>().chars[self.0]
.friends
.iter()
.map(|id| Droid(*id).into())
.collect()
}
#[field(desc = "Which movies they appear in.")]
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
&ctx.data::<StarWars>().chars[self.0].appears_in
}
#[field(desc = "The primary function of the droid.")]
async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
&ctx.data::<StarWars>().chars[self.0].primary_function
}
}
pub struct QueryRoot;
#[async_graphql::Object]
impl QueryRoot {
#[field]
async fn hero(
&self,
ctx: &Context<'_>,
#[arg(
desc = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
)]
episode: Episode,
) -> Character {
if episode == Episode::Empire {
Human(ctx.data::<StarWars>().luke).into()
} else {
Droid(ctx.data::<StarWars>().artoo).into()
}
}
#[field]
async fn human(
&self,
ctx: &Context<'_>,
#[arg(desc = "id of the human")] id: String,
) -> Option<Human> {
ctx.data::<StarWars>().human(&id).map(|id| Human(id))
}
#[field]
async fn humans(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<Connection<Human, EmptyEdgeFields>> {
let humans = ctx
.data::<StarWars>()
.humans()
.iter()
.map(|id| *id)
.collect::<Vec<_>>();
humans
.as_slice()
.query(ctx, after, before, first, last)
.await
.map(|connection| connection.map(|id| Human(*id)))
}
#[field]
async fn droid(
&self,
ctx: &Context<'_>,
#[arg(desc = "id of the droid")] id: String,
) -> Option<Droid> {
ctx.data::<StarWars>().droid(&id).map(|id| Droid(id))
}
#[field]
async fn droids(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<Connection<Droid, EmptyEdgeFields>> {
let droids = ctx
.data::<StarWars>()
.droids()
.iter()
.map(|id| *id)
.collect::<Vec<_>>();
droids
.as_slice()
.query(ctx, after, before, first, last)
.await
.map(|connection| connection.map(|id| Droid(*id)))
}
}
#[async_graphql::Interface(
field(name = "id", type = "&str", context),
field(name = "name", type = "&str", context),
field(name = "friends", type = "Vec<Character>", context),
field(name = "appears_in", type = "&'ctx [Episode]", context)
)]
pub struct Character(Human, Droid);

View File

@ -1,47 +0,0 @@
mod starwars;
use async_graphql::http::{
graphiql_source, playground_source, GQLRequest, GQLResponse, IntoQueryBuilder,
};
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
use futures::TryFutureExt;
use mime;
use tide::{self, Request, Response};
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
async fn index(mut request: Request<StarWarsSchema>) -> Response {
let gql_request: GQLRequest = request.body_json().await.unwrap();
let schema = request.state();
let gql_response = gql_request
.into_query_builder(schema)
.and_then(|builder| builder.execute())
.await;
Response::new(200)
.body_json(&GQLResponse(gql_response))
.unwrap()
}
async fn gql_playground(_request: Request<StarWarsSchema>) -> Response {
Response::new(200)
.body_string(playground_source("/", None))
.set_mime(mime::TEXT_HTML_UTF_8)
}
async fn gql_graphiql(_request: Request<StarWarsSchema>) -> Response {
Response::new(200)
.body_string(graphiql_source("/"))
.set_mime(mime::TEXT_HTML_UTF_8)
}
#[async_std::main]
async fn main() -> std::io::Result<()> {
let mut app = tide::with_state(
Schema::build(starwars::QueryRoot, EmptyMutation, EmptySubscription)
.data(starwars::StarWars::new())
.finish(),
);
app.at("/").post(index);
app.at("/").get(gql_playground);
app.at("/graphiql").get(gql_graphiql);
app.listen("0.0.0.0:8000").await
}

View File

@ -305,7 +305,7 @@ impl From<ParseError> for Error {
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum RequestError {
pub enum ParseRequestError {
#[error("{0}")]
Io(std::io::Error),
@ -353,7 +353,4 @@ pub enum Error {
#[error("Rule error")]
Rule { errors: Vec<RuleError> },
#[error("Request error")]
Request(#[from] RequestError),
}

View File

@ -1,110 +0,0 @@
use crate::error::RequestError;
use crate::http::multipart::{Multipart, PartData};
use crate::http::{GQLRequest, IntoQueryBuilder};
use crate::{Error, ObjectType, QueryBuilder, Result, Schema, SubscriptionType};
use futures::{AsyncRead, AsyncReadExt};
use mime::Mime;
use std::collections::HashMap;
/// Http request trait for GraphQL
#[allow(missing_docs)]
pub trait GQLHttpRequest {
type Body: AsyncRead + Send + Unpin;
fn content_type(&self) -> Option<&str>;
fn into_body(self) -> Self::Body;
}
#[async_trait::async_trait]
impl<R: GQLHttpRequest + Send + Sync> IntoQueryBuilder for R {
async fn into_query_builder<Query, Mutation, Subscription>(
mut self,
schema: &Schema<Query, Mutation, Subscription>,
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
if let Some(boundary) = self
.content_type()
.and_then(|value| value.parse::<Mime>().ok())
.and_then(|ct| {
if ct.essence_str() == mime::MULTIPART_FORM_DATA {
ct.get_param("boundary")
.map(|boundary| boundary.to_string())
} else {
None
}
})
{
// multipart
let mut multipart = Multipart::parse(self.into_body(), boundary.as_str())
.await
.map_err(RequestError::InvalidMultipart)?;
let gql_request: GQLRequest = {
let part = multipart
.remove("operations")
.ok_or_else(|| Error::Request(RequestError::MissingOperatorsPart))?;
let reader = part
.create_reader()
.map_err(|err| Error::Request(RequestError::PartData(err)))?;
serde_json::from_reader(reader).map_err(RequestError::InvalidRequest)?
};
let mut map: HashMap<String, Vec<String>> = {
let part = multipart
.remove("map")
.ok_or_else(|| Error::Request(RequestError::MissingMapPart))?;
let reader = part
.create_reader()
.map_err(|err| Error::Request(RequestError::PartData(err)))?;
serde_json::from_reader(reader).map_err(RequestError::InvalidFilesMap)?
};
let mut builder = gql_request.into_query_builder(schema).await?;
if !builder.is_upload() {
return Err(RequestError::NotUpload.into());
}
// read files
for part in &multipart.parts {
if let Some(name) = &part.name {
if let Some(var_paths) = map.remove(name) {
for var_path in var_paths {
if let (Some(filename), PartData::File(path)) =
(&part.filename, &part.data)
{
builder.set_upload(
&var_path,
&filename,
part.content_type.as_deref(),
path,
);
}
}
}
}
}
if !map.is_empty() {
return Err(RequestError::MissingFiles.into());
}
if let Some(temp_dir) = multipart.temp_dir {
builder.set_files_holder(temp_dir);
}
Ok(builder)
} else {
let mut data = Vec::new();
self.into_body()
.read_to_end(&mut data)
.await
.map_err(RequestError::Io)?;
let gql_request: GQLRequest =
serde_json::from_slice(&data).map_err(RequestError::InvalidRequest)?;
gql_request.into_query_builder(schema).await
}
}
}

View File

@ -0,0 +1,89 @@
use crate::http::multipart::{Multipart, PartData};
use crate::http::GQLRequest;
use crate::query::IntoQueryBuilder;
use crate::{ParseRequestError, QueryBuilder};
use futures::{AsyncRead, AsyncReadExt};
use mime::Mime;
use std::collections::HashMap;
#[async_trait::async_trait]
impl<CT, Body> IntoQueryBuilder for (Option<CT>, Body)
where
CT: AsRef<str> + Send,
Body: AsyncRead + Send + Unpin,
{
async fn into_query_builder(mut self) -> std::result::Result<QueryBuilder, ParseRequestError> {
if let Some(boundary) = self
.0
.and_then(|value| value.as_ref().parse::<Mime>().ok())
.and_then(|ct| {
if ct.essence_str() == mime::MULTIPART_FORM_DATA {
ct.get_param("boundary")
.map(|boundary| boundary.to_string())
} else {
None
}
})
{
// multipart
let mut multipart = Multipart::parse(self.1, boundary.as_str())
.await
.map_err(ParseRequestError::InvalidMultipart)?;
let gql_request: GQLRequest = {
let part = multipart
.remove("operations")
.ok_or_else(|| ParseRequestError::MissingOperatorsPart)?;
let reader = part.create_reader().map_err(ParseRequestError::PartData)?;
serde_json::from_reader(reader).map_err(ParseRequestError::InvalidRequest)?
};
let mut map: HashMap<String, Vec<String>> = {
let part = multipart
.remove("map")
.ok_or_else(|| ParseRequestError::MissingMapPart)?;
let reader = part.create_reader().map_err(ParseRequestError::PartData)?;
serde_json::from_reader(reader).map_err(ParseRequestError::InvalidFilesMap)?
};
let mut builder = gql_request.into_query_builder().await?;
// read files
for part in &multipart.parts {
if let Some(name) = &part.name {
if let Some(var_paths) = map.remove(name) {
for var_path in var_paths {
if let (Some(filename), PartData::File(path)) =
(&part.filename, &part.data)
{
builder.set_upload(
&var_path,
&filename,
part.content_type.as_deref(),
path,
);
}
}
}
}
}
if !map.is_empty() {
return Err(ParseRequestError::MissingFiles);
}
if let Some(temp_dir) = multipart.temp_dir {
builder.set_files_holder(temp_dir);
}
Ok(builder)
} else {
let mut data = Vec::new();
self.1
.read_to_end(&mut data)
.await
.map_err(ParseRequestError::Io)?;
let gql_request: GQLRequest =
serde_json::from_slice(&data).map_err(ParseRequestError::InvalidRequest)?;
gql_request.into_query_builder().await
}
}
}

View File

@ -1,38 +1,24 @@
//! A helper module that supports HTTP
mod graphiql_source;
mod http_request;
mod into_query_builder;
mod multipart;
mod playground_source;
mod stream_body;
mod token_reader;
use itertools::Itertools;
pub use graphiql_source::graphiql_source;
pub use http_request::GQLHttpRequest;
pub use playground_source::playground_source;
pub use stream_body::StreamBody;
use crate::{
Error, ObjectType, QueryBuilder, QueryError, QueryResponse, Result, Schema, SubscriptionType,
Variables,
};
use crate::query::IntoQueryBuilder;
use crate::{Error, ParseRequestError, QueryBuilder, QueryError, QueryResponse, Result, Variables};
use graphql_parser::Pos;
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Serialize, Serializer};
#[allow(missing_docs)]
#[async_trait::async_trait]
pub trait IntoQueryBuilder {
async fn into_query_builder<Query, Mutation, Subscription>(
self,
schema: &Schema<Query, Mutation, Subscription>,
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static;
}
/// Deserializable GraphQL Request object
#[derive(Deserialize, Clone, PartialEq, Debug)]
pub struct GQLRequest {
@ -49,16 +35,8 @@ pub struct GQLRequest {
#[async_trait::async_trait]
impl IntoQueryBuilder for GQLRequest {
async fn into_query_builder<Query, Mutation, Subscription>(
self,
schema: &Schema<Query, Mutation, Subscription>,
) -> Result<QueryBuilder<Query, Mutation, Subscription>>
where
Query: ObjectType + Send + Sync + 'static,
Mutation: ObjectType + Send + Sync + 'static,
Subscription: SubscriptionType + Send + Sync + 'static,
{
let mut builder = schema.query(&self.query)?;
async fn into_query_builder(self) -> std::result::Result<QueryBuilder, ParseRequestError> {
let mut builder = QueryBuilder::new(self.query);
if let Some(operation_name) = self.operation_name {
builder = builder.operator_name(operation_name);
}
@ -160,13 +138,6 @@ impl<'a> Serialize for GQLError<'a> {
}
seq.end()
}
Error::Request(err) => {
let mut seq = serializer.serialize_seq(Some(1))?;
seq.serialize_element(&serde_json::json!({
"message": err.to_string(),
}))?;
seq.end()
}
}
}
}
@ -242,6 +213,7 @@ mod tests {
let resp = GQLResponse(Ok(QueryResponse {
data: json!({"ok": true}),
extensions: None,
cache_control: Default::default(),
}));
assert_eq!(
serde_json::to_value(resp).unwrap(),

View File

@ -1,40 +1,30 @@
use actix_web::web::Payload;
use async_graphql::http::GQLHttpRequest;
use bytes::{Buf, Bytes};
use futures::io::{Error, ErrorKind, Result};
use futures::task::{Context, Poll};
use futures::{AsyncRead, StreamExt};
use futures::{AsyncRead, Stream, StreamExt};
use std::io::{Error, ErrorKind, Result};
use std::pin::Pin;
pub struct RequestWrapper(pub Option<String>, pub Payload);
/// An Adapter for bytes stream to `AsyncRead`
pub struct StreamBody<S> {
s: S,
remain_bytes: Option<Bytes>,
}
unsafe impl Send for RequestWrapper {}
unsafe impl Sync for RequestWrapper {}
impl GQLHttpRequest for RequestWrapper {
type Body = PayloadReader;
fn content_type(&self) -> Option<&str> {
self.0.as_deref()
}
fn into_body(self) -> Self::Body {
PayloadReader {
payload: self.1,
impl<S> StreamBody<S> {
#[allow(missing_docs)]
pub fn new(s: S) -> Self {
Self {
s,
remain_bytes: None,
}
}
}
pub struct PayloadReader {
payload: Payload,
remain_bytes: Option<Bytes>,
}
unsafe impl Send for PayloadReader {}
unsafe impl Sync for PayloadReader {}
impl AsyncRead for PayloadReader {
impl<S, E, D> AsyncRead for StreamBody<S>
where
D: Buf,
S: Stream<Item = std::result::Result<D, E>> + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
@ -49,9 +39,9 @@ impl AsyncRead for PayloadReader {
}
return Poll::Ready(Ok(data.len()));
} else {
match self.payload.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(bytes))) => {
self.remain_bytes = Some(bytes);
match self.s.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(mut bytes))) => {
self.remain_bytes = Some(bytes.to_bytes());
}
Poll::Ready(Some(Err(_))) => {
return Poll::Ready(Err(Error::from(ErrorKind::InvalidData)))

View File

@ -106,10 +106,12 @@ pub mod http;
pub use base::{Scalar, Type};
pub use context::{Context, Environment, QueryPathNode, QueryPathSegment, Variables};
pub use error::{Error, ErrorExtensions, FieldError, FieldResult, QueryError, ResultExt};
pub use error::{
Error, ErrorExtensions, FieldError, FieldResult, ParseRequestError, QueryError, ResultExt,
};
pub use graphql_parser::query::Value;
pub use graphql_parser::Pos;
pub use query::{QueryBuilder, QueryResponse};
pub use query::{IntoQueryBuilder, QueryBuilder, QueryResponse};
pub use registry::CacheControl;
pub use scalars::{Any, ID};
pub use schema::Schema;

View File

@ -1,19 +1,27 @@
use crate::context::Data;
use crate::extensions::BoxExtension;
use crate::error::ParseRequestError;
use crate::mutation_resolver::do_mutation_resolve;
use crate::registry::CacheControl;
use crate::validation::{check_rules, CheckResult};
use crate::{do_resolve, ContextBase, Error, Result, Schema};
use crate::{ObjectType, QueryError, Variables};
use graphql_parser::query::{
Definition, Document, OperationDefinition, SelectionSet, VariableDefinition,
};
use graphql_parser::Pos;
use graphql_parser::{parse_query, Pos};
use itertools::Itertools;
use std::any::Any;
use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use tempdir::TempDir;
#[allow(missing_docs)]
#[async_trait::async_trait]
pub trait IntoQueryBuilder {
async fn into_query_builder(self) -> std::result::Result<QueryBuilder, ParseRequestError>;
}
/// Query response
pub struct QueryResponse {
/// Data of query result
@ -21,59 +29,30 @@ pub struct QueryResponse {
/// Extensions result
pub extensions: Option<serde_json::Map<String, serde_json::Value>>,
/// Cache control value
pub cache_control: CacheControl,
}
/// Query builder
pub struct QueryBuilder<Query, Mutation, Subscription> {
pub(crate) schema: Schema<Query, Mutation, Subscription>,
pub(crate) extensions: Vec<BoxExtension>,
pub(crate) document: Document,
pub struct QueryBuilder {
pub(crate) query_source: String,
pub(crate) operation_name: Option<String>,
pub(crate) variables: Variables,
pub(crate) ctx_data: Option<Data>,
pub(crate) cache_control: CacheControl,
pub(crate) files_holder: Option<TempDir>,
}
impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription> {
fn current_operation(&self) -> Option<(&SelectionSet, &[VariableDefinition], bool)> {
for definition in &self.document.definitions {
match definition {
Definition::Operation(operation_definition) => match operation_definition {
OperationDefinition::SelectionSet(s) => {
return Some((s, &[], true));
}
OperationDefinition::Query(query)
if query.name.is_none()
|| self.operation_name.is_none()
|| query.name.as_deref() == self.operation_name.as_deref() =>
{
return Some((&query.selection_set, &query.variable_definitions, true));
}
OperationDefinition::Mutation(mutation)
if mutation.name.is_none()
|| self.operation_name.is_none()
|| mutation.name.as_deref() == self.operation_name.as_deref() =>
{
return Some((
&mutation.selection_set,
&mutation.variable_definitions,
false,
));
}
OperationDefinition::Subscription(subscription)
if subscription.name.is_none()
|| self.operation_name.is_none()
|| subscription.name.as_deref() == self.operation_name.as_deref() =>
{
return None;
}
_ => {}
},
Definition::Fragment(_) => {}
}
impl QueryBuilder {
/// Create query builder with query source.
pub fn new<T: Into<String>>(query_source: T) -> QueryBuilder {
QueryBuilder {
query_source: query_source.into(),
operation_name: None,
variables: Default::default(),
ctx_data: None,
files_holder: None,
}
None
}
/// Specify the operation name.
@ -101,25 +80,6 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
self
}
/// Detects whether any parameter contains the Upload type
pub fn is_upload(&self) -> bool {
if let Some((_, variable_definitions, _)) = self.current_operation() {
for d in variable_definitions {
if let Some(ty) = self
.schema
.0
.registry
.concrete_type_by_parsed_type(&d.var_type)
{
if ty.name() == "Upload" {
return true;
}
}
}
}
false
}
/// Set file holder
pub fn set_files_holder(&mut self, files_holder: TempDir) {
self.files_holder = Some(files_holder);
@ -138,21 +98,64 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
}
/// Execute the query.
pub async fn execute(self) -> Result<QueryResponse>
pub async fn execute<Query, Mutation, Subscription>(
self,
schema: &Schema<Query, Mutation, Subscription>,
) -> Result<QueryResponse>
where
Query: ObjectType + Send + Sync,
Mutation: ObjectType + Send + Sync,
{
// create extension instances
let extensions = schema
.0
.extensions
.iter()
.map(|factory| factory())
.collect_vec();
// parse query source
extensions
.iter()
.for_each(|e| e.parse_start(&self.query_source));
let document = parse_query(&self.query_source).map_err(Into::<Error>::into)?;
extensions.iter().for_each(|e| e.parse_end());
// check rules
extensions.iter().for_each(|e| e.validation_start());
let CheckResult {
cache_control,
complexity,
depth,
} = check_rules(&schema.0.registry, &document, schema.0.validation_mode)?;
extensions.iter().for_each(|e| e.validation_end());
// check limit
if let Some(limit_complexity) = schema.0.complexity {
if complexity > limit_complexity {
return Err(QueryError::TooComplex.into_error(Pos::default()));
}
}
if let Some(limit_depth) = schema.0.depth {
if depth > limit_depth {
return Err(QueryError::TooDeep.into_error(Pos::default()));
}
}
// execute
let resolve_id = AtomicUsize::default();
let mut fragments = HashMap::new();
let (selection_set, variable_definitions, is_query) =
self.current_operation().ok_or_else(|| Error::Query {
pos: Pos::default(),
path: None,
err: QueryError::MissingOperation,
current_operation(&document, self.operation_name.as_deref()).ok_or_else(|| {
Error::Query {
pos: Pos::default(),
path: None,
err: QueryError::MissingOperation,
}
})?;
for definition in &self.document.definitions {
for definition in &document.definitions {
if let Definition::Fragment(fragment) = &definition {
fragments.insert(fragment.name.clone(), fragment.clone());
}
@ -161,29 +164,29 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
let ctx = ContextBase {
path_node: None,
resolve_id: &resolve_id,
extensions: &self.extensions,
extensions: &extensions,
item: selection_set,
variables: &self.variables,
variable_definitions,
registry: &self.schema.0.registry,
data: &self.schema.0.data,
registry: &schema.0.registry,
data: &schema.0.data,
ctx_data: self.ctx_data.as_ref(),
fragments: &fragments,
};
self.extensions.iter().for_each(|e| e.execution_start());
extensions.iter().for_each(|e| e.execution_start());
let data = if is_query {
do_resolve(&ctx, &self.schema.0.query).await?
do_resolve(&ctx, &schema.0.query).await?
} else {
do_mutation_resolve(&ctx, &self.schema.0.mutation).await?
do_mutation_resolve(&ctx, &schema.0.mutation).await?
};
self.extensions.iter().for_each(|e| e.execution_end());
extensions.iter().for_each(|e| e.execution_end());
let res = QueryResponse {
data,
extensions: if !self.extensions.is_empty() {
extensions: if !extensions.is_empty() {
Some(
self.extensions
extensions
.iter()
.map(|e| (e.name().to_string(), e.result()))
.collect::<serde_json::Map<_, _>>(),
@ -191,12 +194,51 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
} else {
None
},
cache_control,
};
Ok(res)
}
/// Get cache control value
pub fn cache_control(&self) -> CacheControl {
self.cache_control
}
}
fn current_operation<'a>(
document: &'a Document,
operation_name: Option<&str>,
) -> Option<(&'a SelectionSet, &'a [VariableDefinition], bool)> {
for definition in &document.definitions {
match definition {
Definition::Operation(operation_definition) => match operation_definition {
OperationDefinition::SelectionSet(s) => {
return Some((s, &[], true));
}
OperationDefinition::Query(query)
if query.name.is_none()
|| operation_name.is_none()
|| query.name.as_deref() == operation_name.as_deref() =>
{
return Some((&query.selection_set, &query.variable_definitions, true));
}
OperationDefinition::Mutation(mutation)
if mutation.name.is_none()
|| operation_name.is_none()
|| mutation.name.as_deref() == operation_name.as_deref() =>
{
return Some((
&mutation.selection_set,
&mutation.variable_definitions,
false,
));
}
OperationDefinition::Subscription(subscription)
if subscription.name.is_none()
|| operation_name.is_none()
|| subscription.name.as_deref() == operation_name.as_deref() =>
{
return None;
}
_ => {}
},
Definition::Fragment(_) => {}
}
}
None
}

View File

@ -129,21 +129,21 @@ pub struct EnumValue {
/// impl QueryRoot {
/// #[field(cache_control(max_age = 30))]
/// async fn value1(&self) -> i32 {
/// unimplemented!()
/// 0
/// }
///
/// #[field(cache_control(private))]
/// async fn value2(&self) -> i32 {
/// unimplemented!()
/// 0
/// }
/// }
///
/// #[async_std::main]
/// async fn main() {
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
/// assert_eq!(schema.query("{ value1 }").unwrap().cache_control(), CacheControl { public: true, max_age: 30 });
/// assert_eq!(schema.query("{ value2 }").unwrap().cache_control(), CacheControl { public: false, max_age: 60 });
/// assert_eq!(schema.query("{ value1 value2 }").unwrap().cache_control(), CacheControl { public: false, max_age: 30 });
/// assert_eq!(schema.execute("{ value1 }").await.unwrap().cache_control, CacheControl { public: true, max_age: 30 });
/// assert_eq!(schema.execute("{ value2 }").await.unwrap().cache_control, CacheControl { public: false, max_age: 60 });
/// assert_eq!(schema.execute("{ value1 value2 }").await.unwrap().cache_control, CacheControl { public: false, max_age: 30 });
/// }
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Debug)]

View File

@ -5,17 +5,16 @@ use crate::query::QueryBuilder;
use crate::registry::{Directive, InputValue, Registry};
use crate::subscription::{create_connection, create_subscription_stream, SubscriptionTransport};
use crate::types::QueryRoot;
use crate::validation::{check_rules, CheckResult, ValidationMode};
use crate::validation::{check_rules, ValidationMode};
use crate::{
ContextSelectionSet, Error, ObjectType, Pos, QueryError, QueryResponse, Result,
SubscriptionStream, SubscriptionType, Type, Variables,
};
use bytes::Bytes;
use futures::channel::mpsc;
use futures::{Stream, TryFutureExt};
use futures::Stream;
use graphql_parser::parse_query;
use graphql_parser::query::{Definition, OperationDefinition};
use itertools::Itertools;
use std::any::Any;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
@ -224,55 +223,9 @@ where
&self.0.registry
}
/// Start a query and return `QueryBuilder`.
pub fn query(&self, source: &str) -> Result<QueryBuilder<Query, Mutation, Subscription>> {
let extensions = self
.0
.extensions
.iter()
.map(|factory| factory())
.collect_vec();
extensions.iter().for_each(|e| e.parse_start(source));
let document = parse_query(source).map_err(Into::<Error>::into)?;
extensions.iter().for_each(|e| e.parse_end());
extensions.iter().for_each(|e| e.validation_start());
let CheckResult {
cache_control,
complexity,
depth,
} = check_rules(&self.0.registry, &document, self.0.validation_mode)?;
extensions.iter().for_each(|e| e.validation_end());
if let Some(limit_complexity) = self.0.complexity {
if complexity > limit_complexity {
return Err(QueryError::TooComplex.into_error(Pos::default()));
}
}
if let Some(limit_depth) = self.0.depth {
if depth > limit_depth {
return Err(QueryError::TooDeep.into_error(Pos::default()));
}
}
Ok(QueryBuilder {
extensions,
schema: self.clone(),
document,
operation_name: None,
variables: Default::default(),
ctx_data: None,
cache_control,
files_holder: None,
})
}
/// Execute query without create the `QueryBuilder`.
pub async fn execute(&self, source: &str) -> Result<QueryResponse> {
futures::future::ready(self.query(source))
.and_then(|builder| builder.execute())
.await
pub async fn execute(&self, query_source: &str) -> Result<QueryResponse> {
QueryBuilder::new(query_source).execute(self).await
}
/// Create subscription stream, typically called inside the `SubscriptionTransport::handle_request` method

View File

@ -87,7 +87,9 @@ pub struct SubscriptionStream<Query, Mutation, Subscription, T: SubscriptionTran
streams: SubscriptionStreams,
rx_bytes: mpsc::Receiver<Bytes>,
handle_request_fut: Option<
Pin<Box<dyn Future<Output = std::result::Result<Option<Bytes>, T::Error>> + 'static>>,
Pin<
Box<dyn Future<Output = std::result::Result<Option<Bytes>, T::Error>> + Send + 'static>,
>,
>,
}

View File

@ -114,6 +114,7 @@ impl SubscriptionTransport for WebSocketTransport {
serde_json::to_value(GQLResponse(Ok(QueryResponse {
data: value,
extensions: None,
cache_control: Default::default(),
})))
.unwrap(),
),

View File

@ -55,7 +55,7 @@ pub async fn test_enum_type() {
#[async_std::test]
pub async fn test_enum_derive_and_item_attributes() {
use serde::Deserialize;
use serde_derive::Deserialize;
#[async_graphql::Enum]
#[derive(Deserialize, PartialEq, Debug)]

View File

@ -96,7 +96,7 @@ pub async fn test_input_object_default_value() {
#[async_std::test]
pub async fn test_inputobject_derive_and_item_attributes() {
use serde::Deserialize;
use serde_derive::Deserialize;
#[async_graphql::InputObject]
#[derive(Deserialize, PartialEq, Debug)]