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:
parent
bb55ad11cc
commit
93deb2862a
17 changed files with 303 additions and 1065 deletions
|
@ -14,7 +14,6 @@ members = [
|
|||
"juniper_hyper",
|
||||
"juniper_iron",
|
||||
"juniper_rocket",
|
||||
"juniper_rocket_async",
|
||||
"juniper_subscriptions",
|
||||
"juniper_graphql_ws",
|
||||
"juniper_warp",
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
||||
|
|
|
@ -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}}\""},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use rocket::http::Status;
|
||||
|
||||
use juniper_rocket::GraphQLResponse;
|
||||
use rocket::http::Status;
|
||||
|
||||
#[test]
|
||||
fn test_graphql_response_is_public() {
|
||||
|
|
2
juniper_rocket_async/.gitignore
vendored
2
juniper_rocket_async/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
target
|
||||
Cargo.lock
|
|
@ -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).
|
|
@ -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"] }
|
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
|
@ -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");
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
Loading…
Reference in a new issue