Make juniper_rocket_async replace juniper_rocket (#955)

* Make juniper_rocket_async replace juniper_rocket

Now that rocket v0.5 is async and in rc, merge the two juniper projects.

Fixes https://github.com/graphql-rust/juniper/issues/947.

* Remove println
This commit is contained in:
Christian Legnitto 2021-07-07 06:26:22 -10:00 committed by GitHub
parent bb55ad11cc
commit 93deb2862a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 303 additions and 1065 deletions

View file

@ -14,7 +14,6 @@ members = [
"juniper_hyper",
"juniper_iron",
"juniper_rocket",
"juniper_rocket_async",
"juniper_subscriptions",
"juniper_graphql_ws",
"juniper_warp",

View file

@ -12,7 +12,7 @@ CARGO_MAKE_CARGO_ALL_FEATURES = ""
[tasks.release]
condition = { env_set = [ "RELEASE_LEVEL" ] }
workspace = false
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" }
run_task = { name = "release-INTERNAL", fork = true }
@ -39,7 +39,7 @@ args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/releas
[tasks.release-dry-run]
condition = { env_set = [ "RELEASE_LEVEL" ] }
workspace = false
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" }
run_task = { name = "release-dry-run-INTERNAL", fork = true }
[tasks.release-some-dry-run]
@ -53,7 +53,7 @@ run_task = { name = "release-some-dry-run-INTERNAL", fork = true }
[tasks.release-dry-run-INTERNAL]
private = true
condition = { env_set = [ "RELEASE_LEVEL" ] }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" }
description = "Run `cargo-release --dry-run` for every crate"
command = "cargo-release"
args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/release.toml", "--dry-run", "${RELEASE_LEVEL}"]
@ -68,7 +68,7 @@ args = ["release", "--config", "${CARGO_MAKE_WORKING_DIRECTORY}/../_build/releas
[tasks.release-local-test]
condition = { env_set = [ "RELEASE_LEVEL" ] }
workspace = false
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;juniper_rocket_async" }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "integration_tests/*;examples/*;juniper_benchmarks;" }
run_task = { name = "release-local-test-INTERNAL", fork = true }

View file

@ -21,9 +21,6 @@ pre-release-replacements = [
# Rocket
{file="../juniper_rocket/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
{file="../juniper_rocket/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
# Rocket Async
{file="../juniper_rocket_async/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
{file="../juniper_rocket_async/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},
# Warp
{file="../juniper_warp/Cargo.toml", min=0, search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
{file="../juniper_warp/Cargo.toml", min=0, search="\\[dev-dependencies\\.juniper\\]\nversion = \"[^\"]+\"", replace="[dev-dependencies.juniper]\nversion = \"{{version}}\""},

View file

@ -1,5 +1,6 @@
# master
- Require async rocket support (`rocket` >= 0.5-rc1).
- Compatibility with the latest `juniper`.
# [[0.7.1] 2021-06-07](https://github.com/graphql-rust/juniper/releases/tag/juniper_rocket-0.7.1)

View file

@ -12,8 +12,9 @@ documentation = "https://docs.rs/juniper_rocket"
repository = "https://github.com/graphql-rust/juniper"
[dependencies]
juniper = { version = "0.15.6", path = "../juniper", default-features = false}
rocket = { version = "0.4.10", default-features = false }
futures = "0.3.1"
juniper = { version = "0.15.6", path = "../juniper", default-features = false }
rocket = { version = "0.5.0-rc.1", default-features = false }
serde_json = "1.0.2"
[dev-dependencies]

View file

@ -1,17 +0,0 @@
[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

@ -1,11 +1,8 @@
#![feature(decl_macro, proc_macro_hygiene)]
use rocket::{response::content, State};
use juniper::{
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
use rocket::{response::content, Rocket, State};
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
@ -16,24 +13,25 @@ fn graphiql() -> content::Html<String> {
#[rocket::get("/graphql?<request>")]
fn get_graphql_handler(
context: State<Database>,
context: &State<Database>,
request: juniper_rocket::GraphQLRequest,
schema: State<Schema>,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute_sync(&schema, &context)
request.execute_sync(&*schema, &*context)
}
#[rocket::post("/graphql", data = "<request>")]
fn post_graphql_handler(
context: State<Database>,
context: &State<Database>,
request: juniper_rocket::GraphQLRequest,
schema: State<Schema>,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute_sync(&schema, &context)
request.execute_sync(&*schema, &*context)
}
fn main() {
rocket::ignite()
#[rocket::main]
async fn main() {
Rocket::build()
.manage(Database::new())
.manage(Schema::new(
Query,
@ -44,5 +42,7 @@ fn main() {
"/",
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler],
)
.launch();
.launch()
.await
.expect("server to launch");
}

View file

@ -36,23 +36,23 @@ Check the LICENSE file for details.
*/
#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.2.0")]
#![feature(decl_macro, proc_macro_hygiene)]
#![doc(html_root_url = "https://docs.rs/juniper_rocket/0.7.1")]
use std::io::{Cursor, Read};
use std::{borrow::Cow, io::Cursor};
use rocket::{
data::{self, FromData, ToByteUnit},
form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField},
http::{ContentType, Status},
outcome::Outcome::{Failure, Forward, Success},
response::{self, content, Responder, Response},
Data, Request,
};
use juniper::{
http::{self, GraphQLBatchRequest},
DefaultScalarValue, FieldError, GraphQLType, InputValue, RootNode, ScalarValue,
};
use rocket::{
data::{FromDataSimple, Outcome as FromDataOutcome},
http::{ContentType, RawStr, Status},
request::{FormItems, FromForm, FromFormValue},
response::{content, Responder, Response},
Data,
Outcome::{Forward, Success},
Request,
DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync,
InputValue, RootNode, ScalarValue,
};
/// Simple wrapper around an incoming GraphQL request
@ -71,22 +71,22 @@ pub struct GraphQLResponse(pub Status, pub String);
/// Generate an HTML page containing GraphiQL
pub fn graphiql_source(
graphql_endpoint_url: &str,
subscriptions_endpoint: Option<&str>,
subscriptions_endpoint_url: Option<&str>,
) -> content::Html<String> {
content::Html(juniper::http::graphiql::graphiql_source(
graphql_endpoint_url,
subscriptions_endpoint,
subscriptions_endpoint_url,
))
}
/// Generate an HTML page containing GraphQL Playground
pub fn playground_source(
graphql_endpoint_url: &str,
subscriptions_endpoint: Option<&str>,
subscriptions_endpoint_url: Option<&str>,
) -> content::Html<String> {
content::Html(juniper::http::playground::playground_source(
graphql_endpoint_url,
subscriptions_endpoint,
subscriptions_endpoint_url,
))
}
@ -94,7 +94,7 @@ impl<S> GraphQLRequest<S>
where
S: ScalarValue,
{
/// Execute an incoming GraphQL query
/// Synchronously execute an incoming GraphQL query.
pub fn execute_sync<CtxT, QueryT, MutationT, SubscriptionT>(
&self,
root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
@ -116,6 +116,33 @@ where
GraphQLResponse(status, json)
}
/// Asynchronously execute an incoming GraphQL query.
pub async fn execute<CtxT, QueryT, MutationT, SubscriptionT>(
&self,
root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLResponse
where
QueryT: GraphQLTypeAsync<S, Context = CtxT>,
QueryT::TypeInfo: Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT>,
MutationT::TypeInfo: Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT>,
SubscriptionT::TypeInfo: Sync,
CtxT: Sync,
S: Send + Sync,
{
let response = self.0.execute(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.
@ -130,10 +157,8 @@ impl GraphQLResponse {
/// # Examples
///
/// ```
/// # #![feature(decl_macro, proc_macro_hygiene)]
/// #
/// # use rocket::http::Cookies;
/// # use rocket::request::Form;
/// # use rocket::http::CookieJar;
/// # use rocket::form::Form;
/// # use rocket::response::content;
/// # use rocket::State;
/// #
@ -144,17 +169,17 @@ impl GraphQLResponse {
/// #
/// #[rocket::get("/graphql?<request..>")]
/// fn get_graphql_handler(
/// mut cookies: Cookies,
/// context: State<Database>,
/// request: Form<juniper_rocket::GraphQLRequest>,
/// schema: State<Schema>,
/// cookies: &CookieJar,
/// context: &State<Database>,
/// request: juniper_rocket::GraphQLRequest,
/// schema: &State<Schema>,
/// ) -> juniper_rocket::GraphQLResponse {
/// if cookies.get("user_id").is_none() {
/// let err = FieldError::new("User is not logged in", Value::null());
/// return juniper_rocket::GraphQLResponse::error(err);
/// }
///
/// request.execute_sync(&schema, &context)
/// request.execute_sync(&*schema, &*context)
/// }
/// ```
pub fn error(error: FieldError) -> Self {
@ -174,128 +199,172 @@ impl GraphQLResponse {
}
}
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
where
S: ScalarValue,
{
type Error = String;
pub struct GraphQLContext<'f, S: ScalarValue> {
opts: Options,
query: Option<String>,
operation_name: Option<String>,
variables: Option<InputValue<S>>,
errors: Errors<'f>,
}
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;
impl<'f, S: ScalarValue> GraphQLContext<'f, S> {
fn query(&mut self, value: String) {
if self.query.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("query");
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.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.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.to_string()),
}
variables = Some(
serde_json::from_str::<InputValue<_>>(&decoded)
.map_err(|err| err.to_string())?,
);
}
}
_ => {
if strict {
return Err(format!("Prohibited extra field '{}'", key));
}
self.errors.push(error)
} else {
self.query = Some(value);
}
}
fn operation_name(&mut self, value: String) {
if self.operation_name.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("operation_name");
self.errors.push(error)
} else {
self.operation_name = Some(value);
}
}
fn variables(&mut self, value: String) {
if self.variables.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("variables");
self.errors.push(error)
} else {
let parse_result = serde_json::from_str::<InputValue<S>>(&value);
match parse_result {
Ok(variables) => self.variables = Some(variables),
Err(e) => {
let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string())))
.with_name("variables");
self.errors.push(error);
}
}
}
}
}
if let Some(query) = query {
Ok(GraphQLRequest(GraphQLBatchRequest::Single(
http::GraphQLRequest::new(query, operation_name, variables),
)))
} else {
Err("Query parameter missing".to_owned())
#[rocket::async_trait]
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
where
S: ScalarValue + Send,
{
type Context = GraphQLContext<'f, S>;
fn init(opts: Options) -> Self::Context {
GraphQLContext {
opts,
query: None,
operation_name: None,
variables: None,
errors: Errors::new(),
}
}
fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) {
match field.name.key().map(|key| key.as_str()) {
Some("query") => ctx.query(field.value.to_owned()),
Some("operation_name") => ctx.operation_name(field.value.to_owned()),
Some("variables") => ctx.variables(field.value.to_owned()),
Some(key) => {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unknown).with_name(key);
ctx.errors.push(error)
}
}
None => {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unexpected);
ctx.errors.push(error)
}
}
}
}
async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unexpected).with_name(field.name);
ctx.errors.push(error)
}
}
fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> {
if ctx.query.is_none() {
let error = Error::from(ErrorKind::Missing).with_name("query");
ctx.errors.push(error)
}
match ctx.errors.is_empty() {
true => Ok(GraphQLRequest(GraphQLBatchRequest::Single(
http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables),
))),
false => Err(ctx.errors),
}
}
}
impl<'v, S> FromFormValue<'v> for GraphQLRequest<S>
const BODY_LIMIT: u64 = 1024 * 100;
#[rocket::async_trait]
impl<'r, S> FromData<'r> for GraphQLRequest<S>
where
S: ScalarValue,
{
type Error = String;
fn from_form_value(form_value: &'v RawStr) -> Result<Self, Self::Error> {
let mut form_items = FormItems::from(form_value);
async fn from_data(
req: &'r Request<'_>,
data: Data<'r>,
) -> data::Outcome<'r, Self, Self::Error> {
use rocket::tokio::io::AsyncReadExt as _;
Self::from_form(&mut form_items, true)
}
}
impl<S> FromDataSimple for GraphQLRequest<S>
where
S: ScalarValue,
{
type Error = String;
fn from_data(req: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
let content_type = req
.content_type()
.map(|ct| (ct.top().as_str(), ct.sub().as_str()));
let is_json = match content_type {
Some(("application", "json")) => true,
Some(("application", "graphql")) => false,
_ => return Forward(data),
_ => return Box::pin(async move { Forward(data) }).await,
};
let mut body = String::new();
data.open()
.read_to_string(&mut body)
.map_err(|e| (Status::InternalServerError, format!("{:?}", e)))?;
Box::pin(async move {
let mut body = String::new();
let mut reader = data.open(BODY_LIMIT.bytes());
if let Err(e) = reader.read_to_string(&mut body).await {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
Success(GraphQLRequest(if is_json {
serde_json::from_str(&body).map_err(|e| (Status::BadRequest, format!("{}", e)))?
} else {
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
}))
Success(GraphQLRequest(if is_json {
match serde_json::from_str(&body) {
Ok(req) => req,
Err(e) => return Failure((Status::BadRequest, format!("{}", e))),
}
} else {
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
}))
})
.await
}
}
impl<'r> Responder<'r> for GraphQLResponse {
fn respond_to(self, _: &Request) -> Result<Response<'r>, Status> {
impl<'r, 'o: 'r> Responder<'r, 'o> for GraphQLResponse {
fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> {
let GraphQLResponse(status, body) = self;
Ok(Response::build()
Response::build()
.header(ContentType::new("application", "json"))
.status(status)
.sized_body(Cursor::new(body))
.finalize())
.sized_body(body.len(), Cursor::new(body))
.ok()
}
}
@ -303,40 +372,66 @@ impl<'r> Responder<'r> for GraphQLResponse {
mod fromform_tests {
use super::*;
use juniper::InputValue;
use rocket::request::{FormItems, FromForm};
use std::str;
use rocket::{
form::{error::ErrorKind, Error, Form, Strict},
http::RawStr,
};
use std::borrow::Cow;
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);
fn check_error(input: &str, expected_errors: Vec<Error>, strict: bool) {
let errors = if strict {
let result = Form::<Strict<GraphQLRequest>>::parse_encoded(RawStr::new(input));
assert!(result.is_err());
result.unwrap_err()
} else {
let result = Form::<GraphQLRequest>::parse_encoded(RawStr::new(input));
assert!(result.is_err());
result.unwrap_err()
};
assert_eq!(errors.len(), expected_errors.len());
for (error, expected) in errors.iter().zip(&expected_errors) {
match (&error.kind, &expected.kind) {
(ErrorKind::Unknown, ErrorKind::Unknown) => (),
(kind_a, kind_b) => assert_eq!(kind_a, kind_b),
};
assert_eq!(error.name, expected.name);
assert_eq!(error.value, expected.value);
assert_eq!(error.entity, expected.entity);
}
}
#[test]
fn test_empty_form() {
check_error("", "Query parameter missing", false);
check_error(
"",
vec![Error::from(ErrorKind::Missing).with_name("query")],
false,
);
}
#[test]
fn test_no_query() {
check_error(
"operation_name=foo&variables={}",
"Query parameter missing",
vec![Error::from(ErrorKind::Missing).with_name("query")],
false,
);
}
#[test]
fn test_strict() {
check_error("query=test&foo=bar", "Prohibited extra field \'foo\'", true);
check_error(
"query=test&foo=bar",
vec![Error::from(ErrorKind::Unknown).with_name("foo")],
true,
);
}
#[test]
fn test_duplicate_query() {
check_error(
"query=foo&query=bar",
"Query parameter must not occur more than once",
vec![Error::from(ErrorKind::Duplicate).with_name("query")],
false,
);
}
@ -345,7 +440,7 @@ mod fromform_tests {
fn test_duplicate_operation_name() {
check_error(
"query=test&operation_name=op1&operation_name=op2",
"Operation name parameter must not occur more than once",
vec![Error::from(ErrorKind::Duplicate).with_name("operation_name")],
false,
);
}
@ -354,7 +449,7 @@ mod fromform_tests {
fn test_duplicate_variables() {
check_error(
"query=test&variables={}&variables={}",
"Variables parameter must not occur more than once",
vec![Error::from(ErrorKind::Duplicate).with_name("variables")],
false,
);
}
@ -363,16 +458,18 @@ mod fromform_tests {
fn test_variables_invalid_json() {
check_error(
"query=test&variables=NOT_JSON",
"expected value at line 1 column 1",
vec![Error::from(ErrorKind::Validation(Cow::Owned(
"expected value at line 1 column 1".to_owned(),
)))
.with_name("variables")],
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);
let result: Result<GraphQLRequest, Errors> =
Form::parse_encoded(RawStr::new(r#"query=test&variables={"foo":"bar"}"#));
assert!(result.is_ok());
let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"bar"}"#).unwrap();
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
@ -385,10 +482,9 @@ mod fromform_tests {
#[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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new(
r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#,
));
let variables = ::serde_json::from_str::<InputValue>(r#"{"foo":"x y&? z"}"#).unwrap();
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
"test".to_string(),
@ -400,9 +496,9 @@ mod fromform_tests {
#[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);
let result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new(
"query=%25foo%20bar+baz%26%3F&operation_name=test",
));
assert!(result.is_ok());
let expected = GraphQLRequest(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
"%foo bar baz&?".to_string(),
@ -415,6 +511,9 @@ mod fromform_tests {
#[cfg(test)]
mod tests {
use futures;
use juniper::{
http::tests as http_tests,
tests::fixtures::starwars::schema::{Database, Query},
@ -423,30 +522,28 @@ mod tests {
use rocket::{
self, get,
http::ContentType,
local::{Client, LocalRequest},
post,
request::Form,
routes, Rocket, State,
local::asynchronous::{Client, LocalResponse},
post, routes, Build, Rocket, State,
};
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
#[get("/?<request..>")]
fn get_graphql_handler(
context: State<Database>,
request: Form<super::GraphQLRequest>,
schema: State<Schema>,
context: &State<Database>,
request: super::GraphQLRequest,
schema: &State<Schema>,
) -> super::GraphQLResponse {
request.execute_sync(&schema, &context)
request.execute_sync(&*schema, &*context)
}
#[post("/", data = "<request>")]
fn post_graphql_handler(
context: State<Database>,
context: &State<Database>,
request: super::GraphQLRequest,
schema: State<Schema>,
schema: &State<Schema>,
) -> super::GraphQLResponse {
request.execute_sync(&schema, &context)
request.execute_sync(&*schema, &*context)
}
struct TestRocketIntegration {
@ -455,86 +552,90 @@ mod tests {
impl http_tests::HttpIntegration for TestRocketIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse {
let req = &self.client.get(url);
make_test_response(req)
let req = self.client.get(url);
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
let req = &self.client.post(url).header(ContentType::JSON).body(body);
make_test_response(req)
let req = self.client.post(url).header(ContentType::JSON).body(body);
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
let req = &self
let req = self
.client
.post(url)
.header(ContentType::new("application", "graphql"))
.body(body);
make_test_response(req)
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
}
#[test]
fn test_rocket_integration() {
#[rocket::async_test]
async fn test_rocket_integration() {
let rocket = make_rocket();
let client = Client::new(rocket).expect("valid rocket");
let client = Client::untracked(rocket).await.expect("valid rocket");
let integration = TestRocketIntegration { client };
http_tests::run_http_test_suite(&integration);
}
#[test]
fn test_operation_names() {
#[rocket::async_test]
async fn test_operation_names() {
#[post("/", data = "<request>")]
fn post_graphql_assert_operation_name_handler(
context: State<Database>,
context: &State<Database>,
request: super::GraphQLRequest,
schema: State<Schema>,
schema: &State<Schema>,
) -> super::GraphQLResponse {
assert_eq!(request.operation_names(), vec![Some("TestQuery")]);
request.execute_sync(&schema, &context)
request.execute_sync(&*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 client = Client::untracked(rocket).await.expect("valid rocket");
let req = client
let resp = client
.post("/")
.header(ContentType::JSON)
.body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#);
let resp = make_test_response(&req);
.body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#)
.dispatch()
.await;
let resp = make_test_response(resp);
assert_eq!(resp.status_code, 200);
assert_eq!(resp.await.status_code, 200);
}
fn make_rocket() -> Rocket {
fn make_rocket() -> Rocket<Build> {
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(
fn make_rocket_without_routes() -> Rocket<Build> {
Rocket::build().manage(Database::new()).manage(Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
}
fn make_test_response(request: &LocalRequest) -> http_tests::TestResponse {
let mut response = request.clone().dispatch();
async fn make_test_response(response: LocalResponse<'_>) -> http_tests::TestResponse {
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();
.into_string()
.await
.expect("No body returned from GraphQL handler");
http_tests::TestResponse {
status_code,
body,
body: Some(body),
content_type,
}
}

View file

@ -1,6 +1,5 @@
use rocket::http::Status;
use juniper_rocket::GraphQLResponse;
use rocket::http::Status;
#[test]
fn test_graphql_response_is_public() {

View file

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

View file

@ -1,62 +0,0 @@
# master
- Compatibility with the latest `juniper`.
- Rocket integration does not require default features.
- Support `application/graphql` POST requests.
# [[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

@ -1,21 +0,0 @@
[package]
name = "juniper_rocket_async"
version = "0.5.1"
edition = "2018"
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"
[dependencies]
futures = "0.3.1"
juniper = { version = "0.15.6", path = "../juniper", default-features = false }
rocket = { version = "0.5.0-rc.1", default-features = false }
serde_json = "1.0.2"
[dev-dependencies]
juniper = { version = "0.15.6", path = "../juniper", features = ["expose-test-schema"] }

View file

@ -1,25 +0,0 @@
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

@ -1,35 +0,0 @@
# 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

@ -1,48 +0,0 @@
use juniper::{
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
use rocket::{response::content, Rocket, State};
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
#[rocket::get("/")]
fn graphiql() -> content::Html<String> {
juniper_rocket_async::graphiql_source("/graphql", None)
}
#[rocket::get("/graphql?<request>")]
fn get_graphql_handler(
context: &State<Database>,
request: juniper_rocket_async::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket_async::GraphQLResponse {
request.execute_sync(&*schema, &*context)
}
#[rocket::post("/graphql", data = "<request>")]
fn post_graphql_handler(
context: &State<Database>,
request: juniper_rocket_async::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket_async::GraphQLResponse {
request.execute_sync(&*schema, &*context)
}
#[rocket::main]
async fn main() {
Rocket::build()
.manage(Database::new())
.manage(Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
.mount(
"/",
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler],
)
.launch()
.await
.expect("server to launch");
}

View file

@ -1,643 +0,0 @@
/*!
# juniper_rocket_async
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_async
[example]: https://github.com/graphql-rust/juniper_rocket_async/blob/master/examples/rocket_server.rs
*/
#![doc(html_root_url = "https://docs.rs/juniper_rocket_async/0.2.0")]
use std::{borrow::Cow, io::Cursor};
use rocket::{
data::{self, FromData, ToByteUnit},
form::{error::ErrorKind, DataField, Error, Errors, FromForm, Options, ValueField},
http::{ContentType, Status},
outcome::Outcome::{Failure, Forward, Success},
response::{self, content, Responder, Response},
Data, Request,
};
use juniper::{
http::{self, GraphQLBatchRequest},
DefaultScalarValue, FieldError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync,
InputValue, RootNode, ScalarValue,
};
/// 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;
/// 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,
subscriptions_endpoint_url: Option<&str>,
) -> content::Html<String> {
content::Html(juniper::http::graphiql::graphiql_source(
graphql_endpoint_url,
subscriptions_endpoint_url,
))
}
/// Generate an HTML page containing GraphQL Playground
pub fn playground_source(
graphql_endpoint_url: &str,
subscriptions_endpoint_url: Option<&str>,
) -> content::Html<String> {
content::Html(juniper::http::playground::playground_source(
graphql_endpoint_url,
subscriptions_endpoint_url,
))
}
impl<S> GraphQLRequest<S>
where
S: ScalarValue,
{
/// Synchronously execute an incoming GraphQL query.
pub fn execute_sync<CtxT, QueryT, MutationT, SubscriptionT>(
&self,
root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLResponse
where
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
SubscriptionT: GraphQLType<S, Context = CtxT>,
{
let response = self.0.execute_sync(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.
pub async fn execute<CtxT, QueryT, MutationT, SubscriptionT>(
&self,
root_node: &RootNode<'_, QueryT, MutationT, SubscriptionT, S>,
context: &CtxT,
) -> GraphQLResponse
where
QueryT: GraphQLTypeAsync<S, Context = CtxT>,
QueryT::TypeInfo: Sync,
MutationT: GraphQLTypeAsync<S, Context = CtxT>,
MutationT::TypeInfo: Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = CtxT>,
SubscriptionT::TypeInfo: Sync,
CtxT: Sync,
S: Send + Sync,
{
let response = self.0.execute(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
///
/// ```
/// # use rocket::http::CookieJar;
/// # use rocket::form::Form;
/// # use rocket::response::content;
/// # use rocket::State;
/// #
/// # use juniper::tests::fixtures::starwars::schema::{Database, Query};
/// # use juniper::{EmptyMutation, EmptySubscription, FieldError, RootNode, Value};
/// #
/// # type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
/// #
/// #[rocket::get("/graphql?<request..>")]
/// fn get_graphql_handler(
/// cookies: &CookieJar,
/// context: &State<Database>,
/// request: juniper_rocket_async::GraphQLRequest,
/// schema: &State<Schema>,
/// ) -> juniper_rocket_async::GraphQLResponse {
/// if cookies.get("user_id").is_none() {
/// let err = FieldError::new("User is not logged in", Value::null());
/// return juniper_rocket_async::GraphQLResponse::error(err);
/// }
///
/// request.execute_sync(&*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_sync(..).
pub fn custom(status: Status, response: serde_json::Value) -> Self {
let json = serde_json::to_string(&response).unwrap();
GraphQLResponse(status, json)
}
}
pub struct GraphQLContext<'f, S: ScalarValue> {
opts: Options,
query: Option<String>,
operation_name: Option<String>,
variables: Option<InputValue<S>>,
errors: Errors<'f>,
}
impl<'f, S: ScalarValue> GraphQLContext<'f, S> {
fn query(&mut self, value: String) {
if self.query.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("query");
self.errors.push(error)
} else {
println!("{}", value);
self.query = Some(value);
}
}
fn operation_name(&mut self, value: String) {
if self.operation_name.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("operation_name");
self.errors.push(error)
} else {
self.operation_name = Some(value);
}
}
fn variables(&mut self, value: String) {
if self.variables.is_some() {
let error = Error::from(ErrorKind::Duplicate).with_name("variables");
self.errors.push(error)
} else {
let parse_result = serde_json::from_str::<InputValue<S>>(&value);
match parse_result {
Ok(variables) => self.variables = Some(variables),
Err(e) => {
let error = Error::from(ErrorKind::Validation(Cow::Owned(e.to_string())))
.with_name("variables");
self.errors.push(error);
}
}
}
}
}
#[rocket::async_trait]
impl<'f, S> FromForm<'f> for GraphQLRequest<S>
where
S: ScalarValue + Send,
{
type Context = GraphQLContext<'f, S>;
fn init(opts: Options) -> Self::Context {
GraphQLContext {
opts,
query: None,
operation_name: None,
variables: None,
errors: Errors::new(),
}
}
fn push_value(ctx: &mut Self::Context, field: ValueField<'f>) {
match field.name.key().map(|key| key.as_str()) {
Some("query") => ctx.query(field.value.to_owned()),
Some("operation_name") => ctx.operation_name(field.value.to_owned()),
Some("variables") => ctx.variables(field.value.to_owned()),
Some(key) => {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unknown).with_name(key);
ctx.errors.push(error)
}
}
None => {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unexpected);
ctx.errors.push(error)
}
}
}
}
async fn push_data(ctx: &mut Self::Context, field: DataField<'f, '_>) {
if ctx.opts.strict {
let error = Error::from(ErrorKind::Unexpected).with_name(field.name);
ctx.errors.push(error)
}
}
fn finalize(mut ctx: Self::Context) -> rocket::form::Result<'f, Self> {
if ctx.query.is_none() {
let error = Error::from(ErrorKind::Missing).with_name("query");
ctx.errors.push(error)
}
match ctx.errors.is_empty() {
true => Ok(GraphQLRequest(GraphQLBatchRequest::Single(
http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables),
))),
false => Err(ctx.errors),
}
}
}
const BODY_LIMIT: u64 = 1024 * 100;
#[rocket::async_trait]
impl<'r, S> FromData<'r> for GraphQLRequest<S>
where
S: ScalarValue,
{
type Error = String;
async fn from_data(
req: &'r Request<'_>,
data: Data<'r>,
) -> data::Outcome<'r, Self, Self::Error> {
use rocket::tokio::io::AsyncReadExt as _;
let content_type = req
.content_type()
.map(|ct| (ct.top().as_str(), ct.sub().as_str()));
let is_json = match content_type {
Some(("application", "json")) => true,
Some(("application", "graphql")) => false,
_ => return Box::pin(async move { Forward(data) }).await,
};
Box::pin(async move {
let mut body = String::new();
let mut reader = data.open(BODY_LIMIT.bytes());
if let Err(e) = reader.read_to_string(&mut body).await {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
Success(GraphQLRequest(if is_json {
match serde_json::from_str(&body) {
Ok(req) => req,
Err(e) => return Failure((Status::BadRequest, format!("{}", e))),
}
} else {
GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None))
}))
})
.await
}
}
impl<'r, 'o: 'r> Responder<'r, 'o> for GraphQLResponse {
fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> {
let GraphQLResponse(status, body) = self;
Response::build()
.header(ContentType::new("application", "json"))
.status(status)
.sized_body(body.len(), Cursor::new(body))
.ok()
}
}
#[cfg(test)]
mod fromform_tests {
use super::*;
use juniper::InputValue;
use rocket::{
form::{error::ErrorKind, Error, Form, Strict},
http::RawStr,
};
use std::borrow::Cow;
fn check_error(input: &str, expected_errors: Vec<Error>, strict: bool) {
let errors = if strict {
let result = Form::<Strict<GraphQLRequest>>::parse_encoded(RawStr::new(input));
assert!(result.is_err());
result.unwrap_err()
} else {
let result = Form::<GraphQLRequest>::parse_encoded(RawStr::new(input));
assert!(result.is_err());
result.unwrap_err()
};
assert_eq!(errors.len(), expected_errors.len());
for (error, expected) in errors.iter().zip(&expected_errors) {
match (&error.kind, &expected.kind) {
(ErrorKind::Unknown, ErrorKind::Unknown) => (),
(kind_a, kind_b) => assert_eq!(kind_a, kind_b),
};
assert_eq!(error.name, expected.name);
assert_eq!(error.value, expected.value);
assert_eq!(error.entity, expected.entity);
}
}
#[test]
fn test_empty_form() {
check_error(
"",
vec![Error::from(ErrorKind::Missing).with_name("query")],
false,
);
}
#[test]
fn test_no_query() {
check_error(
"operation_name=foo&variables={}",
vec![Error::from(ErrorKind::Missing).with_name("query")],
false,
);
}
#[test]
fn test_strict() {
check_error(
"query=test&foo=bar",
vec![Error::from(ErrorKind::Unknown).with_name("foo")],
true,
);
}
#[test]
fn test_duplicate_query() {
check_error(
"query=foo&query=bar",
vec![Error::from(ErrorKind::Duplicate).with_name("query")],
false,
);
}
#[test]
fn test_duplicate_operation_name() {
check_error(
"query=test&operation_name=op1&operation_name=op2",
vec![Error::from(ErrorKind::Duplicate).with_name("operation_name")],
false,
);
}
#[test]
fn test_duplicate_variables() {
check_error(
"query=test&variables={}&variables={}",
vec![Error::from(ErrorKind::Duplicate).with_name("variables")],
false,
);
}
#[test]
fn test_variables_invalid_json() {
check_error(
"query=test&variables=NOT_JSON",
vec![Error::from(ErrorKind::Validation(Cow::Owned(
"expected value at line 1 column 1".to_owned(),
)))
.with_name("variables")],
false,
);
}
#[test]
fn test_variables_valid_json() {
let result: Result<GraphQLRequest, Errors> =
Form::parse_encoded(RawStr::new(r#"query=test&variables={"foo":"bar"}"#));
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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new(
r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#,
));
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 result: Result<GraphQLRequest, Errors> = Form::parse_encoded(RawStr::new(
"query=%25foo%20bar+baz%26%3F&operation_name=test",
));
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 futures;
use juniper::{
http::tests as http_tests,
tests::fixtures::starwars::schema::{Database, Query},
EmptyMutation, EmptySubscription, RootNode,
};
use rocket::{
self, get,
http::ContentType,
local::asynchronous::{Client, LocalResponse},
post, routes, Build, Rocket, State,
};
type Schema = RootNode<'static, Query, EmptyMutation<Database>, EmptySubscription<Database>>;
#[get("/?<request..>")]
fn get_graphql_handler(
context: &State<Database>,
request: super::GraphQLRequest,
schema: &State<Schema>,
) -> super::GraphQLResponse {
request.execute_sync(&*schema, &*context)
}
#[post("/", data = "<request>")]
fn post_graphql_handler(
context: &State<Database>,
request: super::GraphQLRequest,
schema: &State<Schema>,
) -> super::GraphQLResponse {
request.execute_sync(&*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);
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
fn post_json(&self, url: &str, body: &str) -> http_tests::TestResponse {
let req = self.client.post(url).header(ContentType::JSON).body(body);
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
fn post_graphql(&self, url: &str, body: &str) -> http_tests::TestResponse {
let req = self
.client
.post(url)
.header(ContentType::new("application", "graphql"))
.body(body);
let req = futures::executor::block_on(req.dispatch());
futures::executor::block_on(make_test_response(req))
}
}
#[rocket::async_test]
async fn test_rocket_integration() {
let rocket = make_rocket();
let client = Client::untracked(rocket).await.expect("valid rocket");
let integration = TestRocketIntegration { client };
http_tests::run_http_test_suite(&integration);
}
#[rocket::async_test]
async 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_sync(&*schema, &*context)
}
let rocket = make_rocket_without_routes()
.mount("/", routes![post_graphql_assert_operation_name_handler]);
let client = Client::untracked(rocket).await.expect("valid rocket");
let resp = client
.post("/")
.header(ContentType::JSON)
.body(r#"{"query": "query TestQuery {hero{name}}", "operationName": "TestQuery"}"#)
.dispatch()
.await;
let resp = make_test_response(resp);
assert_eq!(resp.await.status_code, 200);
}
fn make_rocket() -> Rocket<Build> {
make_rocket_without_routes().mount("/", routes![post_graphql_handler, get_graphql_handler])
}
fn make_rocket_without_routes() -> Rocket<Build> {
Rocket::build().manage(Database::new()).manage(Schema::new(
Query,
EmptyMutation::<Database>::new(),
EmptySubscription::<Database>::new(),
))
}
async fn make_test_response(response: LocalResponse<'_>) -> http_tests::TestResponse {
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
.into_string()
.await
.expect("No body returned from GraphQL handler");
http_tests::TestResponse {
status_code,
body: Some(body),
content_type,
}
}
}

View file

@ -1,7 +0,0 @@
use juniper_rocket_async::GraphQLResponse;
use rocket::http::Status;
#[test]
fn test_graphql_response_is_public() {
let _ = GraphQLResponse(Status::Unauthorized, "Unauthorized".to_string());
}