Duplicate juniper_rocket for async
This is required because rocket only supports futures on a git branch.
This commit is contained in:
parent
38c3e28f22
commit
50605ee73e
14 changed files with 928 additions and 103 deletions
|
@ -14,4 +14,6 @@ members = [
|
||||||
exclude = [
|
exclude = [
|
||||||
"docs/book/tests",
|
"docs/book/tests",
|
||||||
"examples/warp_async",
|
"examples/warp_async",
|
||||||
|
# TODO enable async tests
|
||||||
|
"juniper_rocket_async",
|
||||||
]
|
]
|
||||||
|
|
|
@ -666,6 +666,7 @@ pub struct GraphQLTypeDefiniton {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GraphQLTypeDefiniton {
|
impl GraphQLTypeDefiniton {
|
||||||
|
#[allow(unused)]
|
||||||
fn has_async_field(&self) -> bool {
|
fn has_async_field(&self) -> bool {
|
||||||
self.fields.iter().any(|field| field.is_async)
|
self.fields.iter().any(|field| field.is_async)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,18 +11,13 @@ documentation = "https://docs.rs/juniper_rocket"
|
||||||
repository = "https://github.com/graphql-rust/juniper"
|
repository = "https://github.com/graphql-rust/juniper"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[features]
|
|
||||||
async = [ "juniper/async" ]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.2" }
|
serde = { version = "1.0.2" }
|
||||||
serde_json = { version = "1.0.2" }
|
serde_json = { version = "1.0.2" }
|
||||||
serde_derive = { version = "1.0.2" }
|
serde_derive = { version = "1.0.2" }
|
||||||
juniper = { version = "0.14.1", default-features = false, path = "../juniper"}
|
juniper = { version = "0.14.1", default-features = false, path = "../juniper"}
|
||||||
|
|
||||||
futures = { version = "=0.3.1", features = ["compat"] }
|
rocket = { version = "0.4.0" }
|
||||||
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "async" }
|
|
||||||
tokio = "=0.2.0-alpha.6"
|
|
||||||
|
|
||||||
[dev-dependencies.juniper]
|
[dev-dependencies.juniper]
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|
|
@ -39,13 +39,16 @@ Check the LICENSE file for details.
|
||||||
#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")]
|
#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")]
|
||||||
#![feature(decl_macro, proc_macro_hygiene)]
|
#![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
|
||||||
use std::{error::Error, io::Cursor};
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
io::{Cursor, Read},
|
||||||
|
};
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
data::{FromDataFuture, FromDataSimple},
|
data::{FromDataSimple, Outcome as FromDataOutcome},
|
||||||
http::{ContentType, RawStr, Status},
|
http::{ContentType, RawStr, Status},
|
||||||
request::{FormItems, FromForm, FromFormValue},
|
request::{FormItems, FromForm, FromFormValue},
|
||||||
response::{content, Responder, Response, ResultFuture},
|
response::{content, Responder, Response},
|
||||||
Data,
|
Data,
|
||||||
Outcome::{Failure, Forward, Success},
|
Outcome::{Failure, Forward, Success},
|
||||||
Request,
|
Request,
|
||||||
|
@ -54,22 +57,16 @@ use rocket::{
|
||||||
use juniper::{http, InputValue};
|
use juniper::{http, InputValue};
|
||||||
|
|
||||||
use juniper::{
|
use juniper::{
|
||||||
serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode,
|
serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode, ScalarRefValue,
|
||||||
ScalarValue,
|
ScalarValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
|
||||||
use juniper::GraphQLTypeAsync;
|
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
|
||||||
use futures::future::{FutureExt, TryFutureExt};
|
|
||||||
|
|
||||||
#[derive(Debug, serde_derive::Deserialize, PartialEq)]
|
#[derive(Debug, serde_derive::Deserialize, PartialEq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
|
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
|
||||||
enum GraphQLBatchRequest<S = DefaultScalarValue>
|
enum GraphQLBatchRequest<S = DefaultScalarValue>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Sync + Send,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
Single(http::GraphQLRequest<S>),
|
Single(http::GraphQLRequest<S>),
|
||||||
Batch(Vec<http::GraphQLRequest<S>>),
|
Batch(Vec<http::GraphQLRequest<S>>),
|
||||||
|
@ -79,7 +76,7 @@ where
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
|
enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Sync + Send,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
Single(http::GraphQLResponse<'a, S>),
|
Single(http::GraphQLResponse<'a, S>),
|
||||||
Batch(Vec<http::GraphQLResponse<'a, S>>),
|
Batch(Vec<http::GraphQLResponse<'a, S>>),
|
||||||
|
@ -87,7 +84,8 @@ where
|
||||||
|
|
||||||
impl<S> GraphQLBatchRequest<S>
|
impl<S> GraphQLBatchRequest<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync,
|
S: ScalarValue,
|
||||||
|
for<'b> &'b S: ScalarRefValue<'b>,
|
||||||
{
|
{
|
||||||
pub fn execute<'a, CtxT, QueryT, MutationT>(
|
pub fn execute<'a, CtxT, QueryT, MutationT>(
|
||||||
&'a self,
|
&'a self,
|
||||||
|
@ -111,34 +109,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
|
||||||
pub async fn execute_async<'a, CtxT, QueryT, MutationT>(
|
|
||||||
&'a self,
|
|
||||||
root_node: &'a RootNode<'_, QueryT, MutationT, S>,
|
|
||||||
context: &'a CtxT,
|
|
||||||
) -> GraphQLBatchResponse<'a, S>
|
|
||||||
where
|
|
||||||
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
|
||||||
QueryT::TypeInfo: Send + Sync,
|
|
||||||
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
|
||||||
MutationT::TypeInfo: Send + Sync,
|
|
||||||
CtxT: Send + Sync,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
&GraphQLBatchRequest::Single(ref request) => {
|
|
||||||
GraphQLBatchResponse::Single(request.execute_async(root_node, context).await)
|
|
||||||
}
|
|
||||||
&GraphQLBatchRequest::Batch(ref requests) => {
|
|
||||||
let futures = requests
|
|
||||||
.iter()
|
|
||||||
.map(|request| request.execute_async(root_node, context))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
GraphQLBatchResponse::Batch(futures::future::join_all(futures).await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn operation_names(&self) -> Vec<Option<&str>> {
|
pub fn operation_names(&self) -> Vec<Option<&str>> {
|
||||||
match self {
|
match self {
|
||||||
GraphQLBatchRequest::Single(req) => vec![req.operation_name()],
|
GraphQLBatchRequest::Single(req) => vec![req.operation_name()],
|
||||||
|
@ -151,7 +121,7 @@ where
|
||||||
|
|
||||||
impl<'a, S> GraphQLBatchResponse<'a, S>
|
impl<'a, S> GraphQLBatchResponse<'a, S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
fn is_ok(&self) -> bool {
|
fn is_ok(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
@ -171,7 +141,7 @@ where
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct GraphQLRequest<S = DefaultScalarValue>(GraphQLBatchRequest<S>)
|
pub struct GraphQLRequest<S = DefaultScalarValue>(GraphQLBatchRequest<S>)
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync;
|
S: ScalarValue;
|
||||||
|
|
||||||
/// Simple wrapper around the result of executing a GraphQL query
|
/// Simple wrapper around the result of executing a GraphQL query
|
||||||
pub struct GraphQLResponse(pub Status, pub String);
|
pub struct GraphQLResponse(pub Status, pub String);
|
||||||
|
@ -190,7 +160,8 @@ pub fn playground_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
||||||
|
|
||||||
impl<S> GraphQLRequest<S>
|
impl<S> GraphQLRequest<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Sync + Send,
|
S: ScalarValue,
|
||||||
|
for<'b> &'b S: ScalarRefValue<'b>,
|
||||||
{
|
{
|
||||||
/// Execute an incoming GraphQL query
|
/// Execute an incoming GraphQL query
|
||||||
pub fn execute<CtxT, QueryT, MutationT>(
|
pub fn execute<CtxT, QueryT, MutationT>(
|
||||||
|
@ -213,31 +184,6 @@ where
|
||||||
GraphQLResponse(status, json)
|
GraphQLResponse(status, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asynchronously execute an incoming GraphQL query
|
|
||||||
#[cfg(feature = "async")]
|
|
||||||
pub async fn execute_async<CtxT, QueryT, MutationT>(
|
|
||||||
&self,
|
|
||||||
root_node: &RootNode<'_, QueryT, MutationT, S>,
|
|
||||||
context: &CtxT,
|
|
||||||
) -> GraphQLResponse
|
|
||||||
where
|
|
||||||
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
|
||||||
QueryT::TypeInfo: Send + Sync,
|
|
||||||
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
|
||||||
MutationT::TypeInfo: Send + Sync,
|
|
||||||
CtxT: Send + Sync,
|
|
||||||
{
|
|
||||||
let response = self.0.execute_async(root_node, context).await;
|
|
||||||
let status = if response.is_ok() {
|
|
||||||
Status::Ok
|
|
||||||
} else {
|
|
||||||
Status::BadRequest
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string(&response).unwrap();
|
|
||||||
|
|
||||||
GraphQLResponse(status, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the operation names associated with this request.
|
/// Returns the operation names associated with this request.
|
||||||
///
|
///
|
||||||
/// For batch requests there will be multiple names.
|
/// For batch requests there will be multiple names.
|
||||||
|
@ -303,7 +249,7 @@ impl GraphQLResponse {
|
||||||
|
|
||||||
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
|
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
|
@ -374,7 +320,7 @@ where
|
||||||
|
|
||||||
impl<'v, S> FromFormValue<'v> for GraphQLRequest<S>
|
impl<'v, S> FromFormValue<'v> for GraphQLRequest<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
|
@ -385,47 +331,38 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BODY_LIMIT: u64 = 1024 * 100;
|
|
||||||
|
|
||||||
impl<S> FromDataSimple for GraphQLRequest<S>
|
impl<S> FromDataSimple for GraphQLRequest<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue + Send + Sync,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
|
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
|
||||||
use tokio::io::AsyncReadExt as _;
|
|
||||||
|
|
||||||
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
||||||
return Box::pin(async move { Forward(data) });
|
return Forward(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Box::pin(async move {
|
let mut body = String::new();
|
||||||
let mut body = String::new();
|
if let Err(e) = data.open().read_to_string(&mut body) {
|
||||||
let mut reader = data.open().take(BODY_LIMIT);
|
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
||||||
if let Err(e) = reader.read_to_string(&mut body).await {
|
}
|
||||||
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
|
||||||
}
|
|
||||||
|
|
||||||
match serde_json::from_str(&body) {
|
match serde_json::from_str(&body) {
|
||||||
Ok(value) => Success(GraphQLRequest(value)),
|
Ok(value) => Success(GraphQLRequest(value)),
|
||||||
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
|
Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'r> Responder<'r> for GraphQLResponse {
|
impl<'r> Responder<'r> for GraphQLResponse {
|
||||||
fn respond_to(self, _: &Request) -> ResultFuture<'r> {
|
fn respond_to(self, _: &Request) -> Result<Response<'r>, Status> {
|
||||||
let GraphQLResponse(status, body) = self;
|
let GraphQLResponse(status, body) = self;
|
||||||
|
|
||||||
Box::pin(async move {
|
Ok(Response::build()
|
||||||
Ok(Response::build()
|
.header(ContentType::new("application", "json"))
|
||||||
.header(ContentType::new("application", "json"))
|
.status(status)
|
||||||
.status(status)
|
.sized_body(Cursor::new(body))
|
||||||
.sized_body(Cursor::new(body))
|
.finalize())
|
||||||
.finalize())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
juniper_rocket_async/.gitignore
vendored
Normal file
2
juniper_rocket_async/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
Cargo.lock
|
60
juniper_rocket_async/CHANGELOG.md
Normal file
60
juniper_rocket_async/CHANGELOG.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# master
|
||||||
|
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [[0.5.1] 2019-10-24](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.1)
|
||||||
|
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [[0.5.0] 2019-09-29](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.5.0)
|
||||||
|
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [[0.4.1] 2019-07-29](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.4.1)
|
||||||
|
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [[0.4.0] 2019-07-19](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.4.0)
|
||||||
|
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [[0.3.0] 2019-05-16](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.3.0)
|
||||||
|
|
||||||
|
- Expose the operation names from `GraphQLRequest`.
|
||||||
|
- Compatibility with the latest `juniper`.
|
||||||
|
|
||||||
|
# [0.2.0] 2018-12-17
|
||||||
|
|
||||||
|
### Rocket updated to v0.4
|
||||||
|
|
||||||
|
[Rocket](https://rocket.rs) integration now requires Rocket `0.4.0`. This is due
|
||||||
|
to changes with the way Rocket handles form parsing. Before this update, it was
|
||||||
|
impossible to leverage Rocket integration with Rocket beyond 0.3.x.
|
||||||
|
|
||||||
|
Check out [Rocket's Changelog](https://github.com/SergioBenitez/Rocket/blob/v0.4/CHANGELOG.md)
|
||||||
|
for more details on the 0.4 release.
|
||||||
|
|
||||||
|
# juniper_rocket [0.1.3] 2018-09-13
|
||||||
|
|
||||||
|
- Add `juniper-0.10.0` compatibility.
|
||||||
|
|
||||||
|
# juniper_rocket [0.1.2] 2018-01-13
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Rocket updated to `0.3.6`
|
||||||
|
|
||||||
|
[Rocket](https://rocket.rs) integration now requires Rocket `0.3.6` to
|
||||||
|
support building with recent Rust nightlies.
|
||||||
|
|
||||||
|
Additional information and supported nightly versions can be found in [Rocket's changelog](https://github.com/SergioBenitez/Rocket/blob/master/CHANGELOG.md#version-036-jan-12-2018).
|
||||||
|
|
||||||
|
[#125](https://github.com/graphql-rust/juniper/issues/125)
|
||||||
|
|
||||||
|
### Decoding of query params
|
||||||
|
|
||||||
|
When processing GET requests, query parameters were not properly url_decoded,
|
||||||
|
|
||||||
|
This was fixed by [PR #122](https://github.com/graphql-rust/juniper/pull/128) by @LegNeato.
|
||||||
|
|
||||||
|
This fixed the [issue #116](https://github.com/graphql-rust/juniper/issues/116).
|
30
juniper_rocket_async/Cargo.toml
Normal file
30
juniper_rocket_async/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "juniper_rocket"
|
||||||
|
version = "0.5.1"
|
||||||
|
authors = [
|
||||||
|
"Magnus Hallin <mhallin@fastmail.com>",
|
||||||
|
"Christoph Herzog <chris@theduke.at>",
|
||||||
|
]
|
||||||
|
description = "Juniper GraphQL integration with Rocket"
|
||||||
|
license = "BSD-2-Clause"
|
||||||
|
documentation = "https://docs.rs/juniper_rocket"
|
||||||
|
repository = "https://github.com/graphql-rust/juniper"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
async = [ "juniper/async" ]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.2" }
|
||||||
|
serde_json = { version = "1.0.2" }
|
||||||
|
serde_derive = { version = "1.0.2" }
|
||||||
|
juniper = { version = "0.14.1", default-features = false, path = "../juniper"}
|
||||||
|
|
||||||
|
futures = { version = "=0.3.1", features = ["compat"] }
|
||||||
|
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "async" }
|
||||||
|
tokio = "=0.2.0-alpha.6"
|
||||||
|
|
||||||
|
[dev-dependencies.juniper]
|
||||||
|
version = "0.14.1"
|
||||||
|
features = ["expose-test-schema", "serde_json"]
|
||||||
|
path = "../juniper"
|
25
juniper_rocket_async/LICENSE
Normal file
25
juniper_rocket_async/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
BSD 2-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2016, Magnus Hallin
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
17
juniper_rocket_async/Makefile.toml
Normal file
17
juniper_rocket_async/Makefile.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[tasks.build-verbose]
|
||||||
|
condition = { channels = ["nightly"] }
|
||||||
|
|
||||||
|
[tasks.build-verbose.windows]
|
||||||
|
condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } }
|
||||||
|
|
||||||
|
[tasks.test-verbose]
|
||||||
|
condition = { channels = ["nightly"] }
|
||||||
|
|
||||||
|
[tasks.test-verbose.windows]
|
||||||
|
condition = { channels = ["nightly"], env = { "TARGET" = "x86_64-pc-windows-msvc" } }
|
||||||
|
|
||||||
|
[tasks.ci-coverage-flow]
|
||||||
|
condition = { channels = ["nightly"] }
|
||||||
|
|
||||||
|
[tasks.ci-coverage-flow.windows]
|
||||||
|
disabled = true
|
35
juniper_rocket_async/README.md
Normal file
35
juniper_rocket_async/README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# juniper_rocket
|
||||||
|
|
||||||
|
This repository contains the [Rocket][Rocket] web server integration for
|
||||||
|
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For documentation, including guides and examples, check out [Juniper][Juniper].
|
||||||
|
|
||||||
|
A basic usage example can also be found in the [Api documentation][documentation].
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Check [examples/rocket_server.rs][example] for example code of a working Rocket
|
||||||
|
server with GraphQL handlers.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
* [Juniper][Juniper]
|
||||||
|
* [Api Reference][documetation]
|
||||||
|
* [Rocket][Iron]
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is under the BSD-2 license.
|
||||||
|
|
||||||
|
Check the LICENSE file for details.
|
||||||
|
|
||||||
|
[Rocket]: https://rocket.rs
|
||||||
|
[Juniper]: https://github.com/graphql-rust/juniper
|
||||||
|
[GraphQL]: http://graphql.org
|
||||||
|
[documentation]: https://docs.rs/juniper_rocket
|
||||||
|
[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs
|
||||||
|
|
||||||
|
|
44
juniper_rocket_async/examples/rocket_server.rs
Normal file
44
juniper_rocket_async/examples/rocket_server.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
|
||||||
|
use rocket::{response::content, State};
|
||||||
|
|
||||||
|
use juniper::{
|
||||||
|
tests::{model::Database, schema::Query},
|
||||||
|
EmptyMutation, RootNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
|
||||||
|
|
||||||
|
#[rocket::get("/")]
|
||||||
|
fn graphiql() -> content::Html<String> {
|
||||||
|
juniper_rocket::graphiql_source("/graphql")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/graphql?<request>")]
|
||||||
|
fn get_graphql_handler(
|
||||||
|
context: State<Database>,
|
||||||
|
request: juniper_rocket::GraphQLRequest,
|
||||||
|
schema: State<Schema>,
|
||||||
|
) -> juniper_rocket::GraphQLResponse {
|
||||||
|
request.execute(&schema, &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::post("/graphql", data = "<request>")]
|
||||||
|
fn post_graphql_handler(
|
||||||
|
context: State<Database>,
|
||||||
|
request: juniper_rocket::GraphQLRequest,
|
||||||
|
schema: State<Schema>,
|
||||||
|
) -> juniper_rocket::GraphQLResponse {
|
||||||
|
request.execute(&schema, &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
rocket::ignite()
|
||||||
|
.manage(Database::new())
|
||||||
|
.manage(Schema::new(Query, EmptyMutation::<Database>::new()))
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler],
|
||||||
|
)
|
||||||
|
.launch();
|
||||||
|
}
|
8
juniper_rocket_async/release.toml
Normal file
8
juniper_rocket_async/release.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
no-dev-version = true
|
||||||
|
pre-release-commit-message = "Release {{crate_name}} {{version}}"
|
||||||
|
pro-release-commit-message = "Bump {{crate_name}} version to {{next_version}}"
|
||||||
|
tag-message = "Release {{crate_name}} {{version}}"
|
||||||
|
upload-doc = false
|
||||||
|
pre-release-replacements = [
|
||||||
|
{file="src/lib.rs", search="docs.rs/juniper_rocket/[a-z0-9\\.-]+", replace="docs.rs/juniper_rocket/{{version}}"},
|
||||||
|
]
|
658
juniper_rocket_async/src/lib.rs
Normal file
658
juniper_rocket_async/src/lib.rs
Normal file
|
@ -0,0 +1,658 @@
|
||||||
|
/*!
|
||||||
|
|
||||||
|
# juniper_rocket
|
||||||
|
|
||||||
|
This repository contains the [Rocket][Rocket] web server integration for
|
||||||
|
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For documentation, including guides and examples, check out [Juniper][Juniper].
|
||||||
|
|
||||||
|
A basic usage example can also be found in the [Api documentation][documentation].
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Check [examples/rocket_server.rs][example] for example code of a working Rocket
|
||||||
|
server with GraphQL handlers.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
* [Juniper][Juniper]
|
||||||
|
* [Api Reference][documentation]
|
||||||
|
* [Rocket][Rocket]
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is under the BSD-2 license.
|
||||||
|
|
||||||
|
Check the LICENSE file for details.
|
||||||
|
|
||||||
|
[Rocket]: https://rocket.rs
|
||||||
|
[Juniper]: https://github.com/graphql-rust/juniper
|
||||||
|
[GraphQL]: http://graphql.org
|
||||||
|
[documentation]: https://docs.rs/juniper_rocket
|
||||||
|
[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")]
|
||||||
|
#![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
|
||||||
|
use std::{error::Error, io::Cursor};
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
data::{FromDataFuture, FromDataSimple},
|
||||||
|
http::{ContentType, RawStr, Status},
|
||||||
|
request::{FormItems, FromForm, FromFormValue},
|
||||||
|
response::{content, Responder, Response, ResultFuture},
|
||||||
|
Data,
|
||||||
|
Outcome::{Failure, Forward, Success},
|
||||||
|
Request,
|
||||||
|
};
|
||||||
|
|
||||||
|
use juniper::{http, InputValue};
|
||||||
|
|
||||||
|
use juniper::{
|
||||||
|
serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode,
|
||||||
|
ScalarValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use juniper::GraphQLTypeAsync;
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use futures::future::{FutureExt, TryFutureExt};
|
||||||
|
|
||||||
|
#[derive(Debug, serde_derive::Deserialize, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
|
||||||
|
enum GraphQLBatchRequest<S = DefaultScalarValue>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Sync + Send,
|
||||||
|
{
|
||||||
|
Single(http::GraphQLRequest<S>),
|
||||||
|
Batch(Vec<http::GraphQLRequest<S>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde_derive::Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Sync + Send,
|
||||||
|
{
|
||||||
|
Single(http::GraphQLResponse<'a, S>),
|
||||||
|
Batch(Vec<http::GraphQLResponse<'a, S>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> GraphQLBatchRequest<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync,
|
||||||
|
{
|
||||||
|
pub fn execute<'a, CtxT, QueryT, MutationT>(
|
||||||
|
&'a self,
|
||||||
|
root_node: &'a RootNode<QueryT, MutationT, S>,
|
||||||
|
context: &CtxT,
|
||||||
|
) -> GraphQLBatchResponse<'a, S>
|
||||||
|
where
|
||||||
|
QueryT: GraphQLType<S, Context = CtxT>,
|
||||||
|
MutationT: GraphQLType<S, Context = CtxT>,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
&GraphQLBatchRequest::Single(ref request) => {
|
||||||
|
GraphQLBatchResponse::Single(request.execute(root_node, context))
|
||||||
|
}
|
||||||
|
&GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
|
||||||
|
requests
|
||||||
|
.iter()
|
||||||
|
.map(|request| request.execute(root_node, context))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
pub async fn execute_async<'a, CtxT, QueryT, MutationT>(
|
||||||
|
&'a self,
|
||||||
|
root_node: &'a RootNode<'_, QueryT, MutationT, S>,
|
||||||
|
context: &'a CtxT,
|
||||||
|
) -> GraphQLBatchResponse<'a, S>
|
||||||
|
where
|
||||||
|
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
||||||
|
QueryT::TypeInfo: Send + Sync,
|
||||||
|
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
||||||
|
MutationT::TypeInfo: Send + Sync,
|
||||||
|
CtxT: Send + Sync,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
&GraphQLBatchRequest::Single(ref request) => {
|
||||||
|
GraphQLBatchResponse::Single(request.execute_async(root_node, context).await)
|
||||||
|
}
|
||||||
|
&GraphQLBatchRequest::Batch(ref requests) => {
|
||||||
|
let futures = requests
|
||||||
|
.iter()
|
||||||
|
.map(|request| request.execute_async(root_node, context))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
GraphQLBatchResponse::Batch(futures::future::join_all(futures).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn operation_names(&self) -> Vec<Option<&str>> {
|
||||||
|
match self {
|
||||||
|
GraphQLBatchRequest::Single(req) => vec![req.operation_name()],
|
||||||
|
GraphQLBatchRequest::Batch(reqs) => {
|
||||||
|
reqs.iter().map(|req| req.operation_name()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S> GraphQLBatchResponse<'a, S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync,
|
||||||
|
{
|
||||||
|
fn is_ok(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
&GraphQLBatchResponse::Single(ref response) => response.is_ok(),
|
||||||
|
&GraphQLBatchResponse::Batch(ref responses) => responses
|
||||||
|
.iter()
|
||||||
|
.fold(true, |ok, response| ok && response.is_ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple wrapper around an incoming GraphQL request
|
||||||
|
///
|
||||||
|
/// See the `http` module for more information. This type can be constructed
|
||||||
|
/// automatically from both GET and POST routes by implementing the `FromForm`
|
||||||
|
/// and `FromData` traits.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct GraphQLRequest<S = DefaultScalarValue>(GraphQLBatchRequest<S>)
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync;
|
||||||
|
|
||||||
|
/// Simple wrapper around the result of executing a GraphQL query
|
||||||
|
pub struct GraphQLResponse(pub Status, pub String);
|
||||||
|
|
||||||
|
/// Generate an HTML page containing GraphiQL
|
||||||
|
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
||||||
|
content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an HTML page containing GraphQL Playground
|
||||||
|
pub fn playground_source(graphql_endpoint_url: &str) -> content::Html<String> {
|
||||||
|
content::Html(juniper::http::playground::playground_source(
|
||||||
|
graphql_endpoint_url,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> GraphQLRequest<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Sync + Send,
|
||||||
|
{
|
||||||
|
/// Execute an incoming GraphQL query
|
||||||
|
pub fn execute<CtxT, QueryT, MutationT>(
|
||||||
|
&self,
|
||||||
|
root_node: &RootNode<QueryT, MutationT, S>,
|
||||||
|
context: &CtxT,
|
||||||
|
) -> GraphQLResponse
|
||||||
|
where
|
||||||
|
QueryT: GraphQLType<S, Context = CtxT>,
|
||||||
|
MutationT: GraphQLType<S, Context = CtxT>,
|
||||||
|
{
|
||||||
|
let response = self.0.execute(root_node, context);
|
||||||
|
let status = if response.is_ok() {
|
||||||
|
Status::Ok
|
||||||
|
} else {
|
||||||
|
Status::BadRequest
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
||||||
|
GraphQLResponse(status, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronously execute an incoming GraphQL query
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
pub async fn execute_async<CtxT, QueryT, MutationT>(
|
||||||
|
&self,
|
||||||
|
root_node: &RootNode<'_, QueryT, MutationT, S>,
|
||||||
|
context: &CtxT,
|
||||||
|
) -> GraphQLResponse
|
||||||
|
where
|
||||||
|
QueryT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
||||||
|
QueryT::TypeInfo: Send + Sync,
|
||||||
|
MutationT: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
|
||||||
|
MutationT::TypeInfo: Send + Sync,
|
||||||
|
CtxT: Send + Sync,
|
||||||
|
{
|
||||||
|
let response = self.0.execute_async(root_node, context).await;
|
||||||
|
let status = if response.is_ok() {
|
||||||
|
Status::Ok
|
||||||
|
} else {
|
||||||
|
Status::BadRequest
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
|
||||||
|
GraphQLResponse(status, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the operation names associated with this request.
|
||||||
|
///
|
||||||
|
/// For batch requests there will be multiple names.
|
||||||
|
pub fn operation_names(&self) -> Vec<Option<&str>> {
|
||||||
|
self.0.operation_names()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphQLResponse {
|
||||||
|
/// Constructs an error response outside of the normal execution flow
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # #![feature(decl_macro, proc_macro_hygiene)]
|
||||||
|
/// #
|
||||||
|
/// # extern crate juniper;
|
||||||
|
/// # extern crate juniper_rocket;
|
||||||
|
/// # extern crate rocket;
|
||||||
|
/// #
|
||||||
|
/// # use rocket::http::Cookies;
|
||||||
|
/// # use rocket::request::Form;
|
||||||
|
/// # use rocket::response::content;
|
||||||
|
/// # use rocket::State;
|
||||||
|
/// #
|
||||||
|
/// # use juniper::tests::schema::Query;
|
||||||
|
/// # use juniper::tests::model::Database;
|
||||||
|
/// # use juniper::{EmptyMutation, FieldError, RootNode, Value};
|
||||||
|
/// #
|
||||||
|
/// # type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
|
||||||
|
/// #
|
||||||
|
/// #[rocket::get("/graphql?<request..>")]
|
||||||
|
/// fn get_graphql_handler(
|
||||||
|
/// mut cookies: Cookies,
|
||||||
|
/// context: State<Database>,
|
||||||
|
/// request: Form<juniper_rocket::GraphQLRequest>,
|
||||||
|
/// schema: State<Schema>,
|
||||||
|
/// ) -> juniper_rocket::GraphQLResponse {
|
||||||
|
/// if cookies.get_private("user_id").is_none() {
|
||||||
|
/// let err = FieldError::new("User is not logged in", Value::null());
|
||||||
|
/// return juniper_rocket::GraphQLResponse::error(err);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// request.execute(&schema, &context)
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn error(error: FieldError) -> Self {
|
||||||
|
let response = http::GraphQLResponse::error(error);
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
GraphQLResponse(Status::BadRequest, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a custom response outside of the normal execution flow
|
||||||
|
///
|
||||||
|
/// This is intended for highly customized integrations and should only
|
||||||
|
/// be used as a last resort. For normal juniper use, use the response
|
||||||
|
/// from GraphQLRequest::execute(..).
|
||||||
|
pub fn custom(status: Status, response: serde_json::Value) -> Self {
|
||||||
|
let json = serde_json::to_string(&response).unwrap();
|
||||||
|
GraphQLResponse(status, json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync,
|
||||||
|
{
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result<Self, String> {
|
||||||
|
let mut query = None;
|
||||||
|
let mut operation_name = None;
|
||||||
|
let mut variables = None;
|
||||||
|
|
||||||
|
for form_item in form_items {
|
||||||
|
let (key, value) = form_item.key_value();
|
||||||
|
// Note: we explicitly decode in the match arms to save work rather
|
||||||
|
// than decoding every form item blindly.
|
||||||
|
match key.as_str() {
|
||||||
|
"query" => {
|
||||||
|
if query.is_some() {
|
||||||
|
return Err("Query parameter must not occur more than once".to_owned());
|
||||||
|
} else {
|
||||||
|
match value.url_decode() {
|
||||||
|
Ok(v) => query = Some(v),
|
||||||
|
Err(e) => return Err(e.description().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"operation_name" => {
|
||||||
|
if operation_name.is_some() {
|
||||||
|
return Err(
|
||||||
|
"Operation name parameter must not occur more than once".to_owned()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
match value.url_decode() {
|
||||||
|
Ok(v) => operation_name = Some(v),
|
||||||
|
Err(e) => return Err(e.description().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"variables" => {
|
||||||
|
if variables.is_some() {
|
||||||
|
return Err("Variables parameter must not occur more than once".to_owned());
|
||||||
|
} else {
|
||||||
|
let decoded;
|
||||||
|
match value.url_decode() {
|
||||||
|
Ok(v) => decoded = v,
|
||||||
|
Err(e) => return Err(e.description().to_string()),
|
||||||
|
}
|
||||||
|
variables = Some(
|
||||||
|
serde_json::from_str::<InputValue<_>>(&decoded)
|
||||||
|
.map_err(|err| err.description().to_owned())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if strict {
|
||||||
|
return Err(format!("Prohibited extra field '{}'", key).to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(query) = query {
|
||||||
|
Ok(GraphQLRequest(GraphQLBatchRequest::Single(
|
||||||
|
http::GraphQLRequest::new(query, operation_name, variables),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err("Query parameter missing".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'v, S> FromFormValue<'v> for GraphQLRequest<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync,
|
||||||
|
{
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn from_form_value(form_value: &'v RawStr) -> Result<Self, Self::Error> {
|
||||||
|
let mut form_items = FormItems::from(form_value);
|
||||||
|
|
||||||
|
Self::from_form(&mut form_items, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BODY_LIMIT: u64 = 1024 * 100;
|
||||||
|
|
||||||
|
impl<S> FromDataSimple for GraphQLRequest<S>
|
||||||
|
where
|
||||||
|
S: ScalarValue + Send + Sync,
|
||||||
|
{
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
|
||||||
|
use tokio::io::AsyncReadExt as _;
|
||||||
|
|
||||||
|
if !request.content_type().map_or(false, |ct| ct.is_json()) {
|
||||||
|
return Box::pin(async move { Forward(data) });
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut body = String::new();
|
||||||
|
let mut reader = data.open().take(BODY_LIMIT);
|
||||||
|
if let Err(e) = reader.read_to_string(&mut body).await {
|
||||||
|
return Failure((Status::InternalServerError, format!("{:?}", e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match serde_json::from_str(&body) {
|
||||||
|
Ok(value) => Success(GraphQLRequest(value)),
|
||||||
|
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Responder<'r> for GraphQLResponse {
|
||||||
|
fn respond_to(self, _: &Request) -> ResultFuture<'r> {
|
||||||
|
let GraphQLResponse(status, body) = self;
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
Ok(Response::build()
|
||||||
|
.header(ContentType::new("application", "json"))
|
||||||
|
.status(status)
|
||||||
|
.sized_body(Cursor::new(body))
|
||||||
|
.finalize())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fromform_tests {
|
||||||
|
use super::*;
|
||||||
|
use juniper::InputValue;
|
||||||
|
use rocket::request::{FormItems, FromForm};
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
fn check_error(input: &str, error: &str, strict: bool) {
|
||||||
|
let mut items = FormItems::from(input);
|
||||||
|
let result: Result<GraphQLRequest, _> = GraphQLRequest::from_form(&mut items, strict);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(result.unwrap_err(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_form() {
|
||||||
|
check_error("", "Query parameter missing", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_query() {
|
||||||
|
check_error(
|
||||||
|
"operation_name=foo&variables={}",
|
||||||
|
"Query parameter missing",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strict() {
|
||||||
|
check_error("query=test&foo=bar", "Prohibited extra field \'foo\'", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_query() {
|
||||||
|
check_error(
|
||||||
|
"query=foo&query=bar",
|
||||||
|
"Query parameter must not occur more than once",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_operation_name() {
|
||||||
|
check_error(
|
||||||
|
"query=test&operation_name=op1&operation_name=op2",
|
||||||
|
"Operation name parameter must not occur more than once",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_variables() {
|
||||||
|
check_error(
|
||||||
|
"query=test&variables={}&variables={}",
|
||||||
|
"Variables parameter must not occur more than once",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variables_invalid_json() {
|
||||||
|
check_error("query=test&variables=NOT_JSON", "JSON error", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variables_valid_json() {
|
||||||
|
let form_string = r#"query=test&variables={"foo":"bar"}"#;
|
||||||
|
let mut items = FormItems::from(form_string);
|
||||||
|
let result = GraphQLRequest::from_form(&mut items, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"bar"}"#).unwrap();
|
||||||
|
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||||
|
"test".to_string(),
|
||||||
|
None,
|
||||||
|
Some(variables),
|
||||||
|
)));
|
||||||
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variables_encoded_json() {
|
||||||
|
let form_string = r#"query=test&variables={"foo": "x%20y%26%3F+z"}"#;
|
||||||
|
let mut items = FormItems::from(form_string);
|
||||||
|
let result = GraphQLRequest::from_form(&mut items, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"x y&? z"}"#).unwrap();
|
||||||
|
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||||
|
"test".to_string(),
|
||||||
|
None,
|
||||||
|
Some(variables),
|
||||||
|
)));
|
||||||
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_url_decode() {
|
||||||
|
let form_string = "query=%25foo%20bar+baz%26%3F&operation_name=test";
|
||||||
|
let mut items = FormItems::from(form_string);
|
||||||
|
let result: Result<GraphQLRequest, _> = GraphQLRequest::from_form(&mut items, false);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
|
||||||
|
"%foo bar baz&?".to_string(),
|
||||||
|
Some("test".to_string()),
|
||||||
|
None,
|
||||||
|
)));
|
||||||
|
assert_eq!(result.unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
self, get,
|
||||||
|
http::ContentType,
|
||||||
|
local::{Client, LocalRequest},
|
||||||
|
post,
|
||||||
|
request::Form,
|
||||||
|
routes, Rocket, State,
|
||||||
|
};
|
||||||
|
|
||||||
|
use juniper::{
|
||||||
|
http::tests as http_tests,
|
||||||
|
tests::{model::Database, schema::Query},
|
||||||
|
EmptyMutation, RootNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Schema = RootNode<'static, Query, EmptyMutation<Database>>;
|
||||||
|
|
||||||
|
#[get("/?<request..>")]
|
||||||
|
fn get_graphql_handler(
|
||||||
|
context: State<Database>,
|
||||||
|
request: Form<super::GraphQLRequest>,
|
||||||
|
schema: State<Schema>,
|
||||||
|
) -> super::GraphQLResponse {
|
||||||
|
request.execute(&schema, &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/", data = "<request>")]
|
||||||
|
fn post_graphql_handler(
|
||||||
|
context: State<Database>,
|
||||||
|
request: super::GraphQLRequest,
|
||||||
|
schema: State<Schema>,
|
||||||
|
) -> super::GraphQLResponse {
|
||||||
|
request.execute(&schema, &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestRocketIntegration {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl http_tests::HTTPIntegration for TestRocketIntegration {
|
||||||
|
fn get(&self, url: &str) -> http_tests::TestResponse {
|
||||||
|
let req = &self.client.get(url);
|
||||||
|
make_test_response(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
|
||||||
|
let req = &self.client.post(url).header(ContentType::JSON).body(body);
|
||||||
|
make_test_response(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rocket_integration() {
|
||||||
|
let rocket = make_rocket();
|
||||||
|
let client = Client::new(rocket).expect("valid rocket");
|
||||||
|
let integration = TestRocketIntegration { client };
|
||||||
|
|
||||||
|
http_tests::run_http_test_suite(&integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_operation_names() {
|
||||||
|
#[post("/", data = "<request>")]
|
||||||
|
fn post_graphql_assert_operation_name_handler(
|
||||||
|
context: State<Database>,
|
||||||
|
request: super::GraphQLRequest,
|
||||||
|
schema: State<Schema>,
|
||||||
|
) -> super::GraphQLResponse {
|
||||||
|
assert_eq!(request.operation_names(), vec![Some("TestQuery")]);
|
||||||
|
request.execute(&schema, &context)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rocket = make_rocket_without_routes()
|
||||||
|
.mount("/", routes![post_graphql_assert_operation_name_handler]);
|
||||||
|
let client = Client::new(rocket).expect("valid rocket");
|
||||||
|
|
||||||
|
let req = client
|
||||||
|
.post("/")
|
||||||
|
.header(ContentType::JSON)
|
||||||
|
.body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#);
|
||||||
|
let resp = make_test_response(&req);
|
||||||
|
|
||||||
|
assert_eq!(resp.status_code, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_rocket() -> Rocket {
|
||||||
|
make_rocket_without_routes().mount("/", routes![post_graphql_handler, get_graphql_handler])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_rocket_without_routes() -> Rocket {
|
||||||
|
rocket::ignite()
|
||||||
|
.manage(Database::new())
|
||||||
|
.manage(Schema::new(Query, EmptyMutation::<Database>::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse {
|
||||||
|
let mut response = request.clone().dispatch();
|
||||||
|
let status_code = response.status().code as i32;
|
||||||
|
let content_type = response
|
||||||
|
.content_type()
|
||||||
|
.expect("No content type header from handler")
|
||||||
|
.to_string();
|
||||||
|
let body = response
|
||||||
|
.body()
|
||||||
|
.expect("No body returned from GraphQL handler")
|
||||||
|
.into_string();
|
||||||
|
|
||||||
|
http_tests::TestResponse {
|
||||||
|
status_code,
|
||||||
|
body,
|
||||||
|
content_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
juniper_rocket_async/tests/custom_response_tests.rs
Normal file
11
juniper_rocket_async/tests/custom_response_tests.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
extern crate juniper_rocket;
|
||||||
|
extern crate rocket;
|
||||||
|
|
||||||
|
use rocket::http::Status;
|
||||||
|
|
||||||
|
use juniper_rocket::GraphQLResponse;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_graphql_response_is_public() {
|
||||||
|
let _ = GraphQLResponse(Status::Unauthorized, "Unauthorized".to_string());
|
||||||
|
}
|
Loading…
Reference in a new issue