Duplicate juniper_rocket for async

This is required because rocket only supports futures on a git branch.
This commit is contained in:
Christoph Herzog 2019-11-15 21:50:35 +01:00
parent 38c3e28f22
commit 50605ee73e
14 changed files with 928 additions and 103 deletions

View file

@ -14,4 +14,6 @@ members = [
exclude = [
"docs/book/tests",
"examples/warp_async",
# TODO enable async tests
"juniper_rocket_async",
]

View file

@ -666,6 +666,7 @@ pub struct GraphQLTypeDefiniton {
}
impl GraphQLTypeDefiniton {
#[allow(unused)]
fn has_async_field(&self) -> bool {
self.fields.iter().any(|field| field.is_async)
}

View file

@ -11,18 +11,13 @@ 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"
rocket = { version = "0.4.0" }
[dev-dependencies.juniper]
version = "0.14.1"

View file

@ -39,13 +39,16 @@ Check the LICENSE file for details.
#![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 std::{
error::Error,
io::{Cursor, Read},
};
use rocket::{
data::{FromDataFuture, FromDataSimple},
data::{FromDataSimple, Outcome as FromDataOutcome},
http::{ContentType, RawStr, Status},
request::{FormItems, FromForm, FromFormValue},
response::{content, Responder, Response, ResultFuture},
response::{content, Responder, Response},
Data,
Outcome::{Failure, Forward, Success},
Request,
@ -54,22 +57,16 @@ use rocket::{
use juniper::{http, InputValue};
use juniper::{
serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode,
serde::Deserialize, DefaultScalarValue, FieldError, GraphQLType, RootNode, ScalarRefValue,
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,
S: ScalarValue,
{
Single(http::GraphQLRequest<S>),
Batch(Vec<http::GraphQLRequest<S>>),
@ -79,7 +76,7 @@ where
#[serde(untagged)]
enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
where
S: ScalarValue + Sync + Send,
S: ScalarValue,
{
Single(http::GraphQLResponse<'a, S>),
Batch(Vec<http::GraphQLResponse<'a, S>>),
@ -87,7 +84,8 @@ where
impl<S> GraphQLBatchRequest<S>
where
S: ScalarValue + Send + Sync,
S: ScalarValue,
for<'b> &'b S: ScalarRefValue<'b>,
{
pub fn execute<'a, CtxT, QueryT, MutationT>(
&'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>> {
match self {
GraphQLBatchRequest::Single(req) => vec![req.operation_name()],
@ -151,7 +121,7 @@ where
impl<'a, S> GraphQLBatchResponse<'a, S>
where
S: ScalarValue + Send + Sync,
S: ScalarValue,
{
fn is_ok(&self) -> bool {
match self {
@ -171,7 +141,7 @@ where
#[derive(Debug, PartialEq)]
pub struct GraphQLRequest<S = DefaultScalarValue>(GraphQLBatchRequest<S>)
where
S: ScalarValue + Send + Sync;
S: ScalarValue;
/// Simple wrapper around the result of executing a GraphQL query
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>
where
S: ScalarValue + Sync + Send,
S: ScalarValue,
for<'b> &'b S: ScalarRefValue<'b>,
{
/// Execute an incoming GraphQL query
pub fn execute<CtxT, QueryT, MutationT>(
@ -213,31 +184,6 @@ where
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.
@ -303,7 +249,7 @@ impl GraphQLResponse {
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
where
S: ScalarValue + Send + Sync,
S: ScalarValue,
{
type Error = String;
@ -374,7 +320,7 @@ where
impl<'v, S> FromFormValue<'v> for GraphQLRequest<S>
where
S: ScalarValue + Send + Sync,
S: ScalarValue,
{
type Error = String;
@ -385,47 +331,38 @@ where
}
}
const BODY_LIMIT: u64 = 1024 * 100;
impl<S> FromDataSimple for GraphQLRequest<S>
where
S: ScalarValue + Send + Sync,
S: ScalarValue,
{
type Error = String;
fn from_data(request: &Request, data: Data) -> FromDataFuture<'static, Self, Self::Error> {
use tokio::io::AsyncReadExt as _;
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
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 reader = data.open().take(BODY_LIMIT);
if let Err(e) = reader.read_to_string(&mut body).await {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
let mut body = String::new();
if let Err(e) = data.open().read_to_string(&mut body) {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
match serde_json::from_str(&body) {
Ok(value) => Success(GraphQLRequest(value)),
Err(failure) => Failure((Status::BadRequest, format!("{}", failure))),
}
})
match serde_json::from_str(&body) {
Ok(value) => Success(GraphQLRequest(value)),
Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))),
}
}
}
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;
Box::pin(async move {
Ok(Response::build()
.header(ContentType::new("application", "json"))
.status(status)
.sized_body(Cursor::new(body))
.finalize())
})
Ok(Response::build()
.header(ContentType::new("application", "json"))
.status(status)
.sized_body(Cursor::new(body))
.finalize())
}
}

2
juniper_rocket_async/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
Cargo.lock

View 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).

View 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"

View 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.

View 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

View 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

View 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();
}

View 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}}"},
]

View 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,
}
}
}

View 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());
}