#![doc(html_root_url = "https://docs.rs/juniper_iron/0.3.0")]
#[cfg(test)]
extern crate iron_test;
#[cfg(test)]
extern crate url;
use iron::{itry, method, middleware::Handler, mime::Mime, prelude::*, status};
use urlencoded::{UrlDecodingError, UrlEncodedQuery};
use std::{error::Error, fmt, io::Read};
use serde_json::error::Error as SerdeError;
use juniper::{
http, serde::Deserialize, DefaultScalarValue, GraphQLType, InputValue, RootNode,
ScalarRefValue, ScalarValue,
};
#[derive(serde_derive::Deserialize)]
#[serde(untagged)]
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
enum GraphQLBatchRequest<S = DefaultScalarValue>
where
S: ScalarValue,
{
Single(http::GraphQLRequest<S>),
Batch(Vec<http::GraphQLRequest<S>>),
}
#[derive(serde_derive::Serialize)]
#[serde(untagged)]
enum GraphQLBatchResponse<'a, S = DefaultScalarValue>
where
S: ScalarValue,
{
Single(http::GraphQLResponse<'a, S>),
Batch(Vec<http::GraphQLResponse<'a, S>>),
}
impl<S> GraphQLBatchRequest<S>
where
S: ScalarValue,
for<'b> &'b S: ScalarRefValue<'b>,
{
pub fn execute<'a, CtxT, QueryT, MutationT>(
&'a self,
root_node: &'a RootNode<QueryT, MutationT, S>,
context: &CtxT,
) -> GraphQLBatchResponse<'a, S>
where
QueryT: GraphQLType<S, Context = CtxT>,
MutationT: GraphQLType<S, Context = CtxT>,
{
match self {
&GraphQLBatchRequest::Single(ref request) => {
GraphQLBatchResponse::Single(request.execute(root_node, context))
}
&GraphQLBatchRequest::Batch(ref requests) => GraphQLBatchResponse::Batch(
requests
.iter()
.map(|request| request.execute(root_node, context))
.collect(),
),
}
}
}
impl<'a, S> GraphQLBatchResponse<'a, S>
where
S: ScalarValue,
{
fn is_ok(&self) -> bool {
match self {
&GraphQLBatchResponse::Single(ref response) => response.is_ok(),
&GraphQLBatchResponse::Batch(ref responses) => responses
.iter()
.fold(true, |ok, response| ok && response.is_ok()),
}
}
}
pub struct GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S = DefaultScalarValue>
where
S: ScalarValue,
for<'b> &'b S: ScalarRefValue<'b>,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT> + Send + Sync + 'static,
{
context_factory: CtxFactory,
root_node: RootNode<'a, Query, Mutation, S>,
}
pub struct GraphiQLHandler {
graphql_url: String,
}
pub struct PlaygroundHandler {
graphql_url: String,
}
fn get_single_value<T>(mut values: Vec<T>) -> IronResult<T> {
if values.len() == 1 {
Ok(values.remove(0))
} else {
Err(GraphQLIronError::InvalidData("Duplicate URL query parameter").into())
}
}
fn parse_url_param(params: Option<Vec<String>>) -> IronResult<Option<String>> {
if let Some(values) = params {
get_single_value(values).map(Some)
} else {
Ok(None)
}
}
fn parse_variable_param<S>(params: Option<Vec<String>>) -> IronResult<Option<InputValue<S>>>
where
S: ScalarValue,
{
if let Some(values) = params {
Ok(
serde_json::from_str::<InputValue<S>>(get_single_value(values)?.as_ref())
.map(Some)
.map_err(GraphQLIronError::Serde)?,
)
} else {
Ok(None)
}
}
impl<'a, CtxFactory, Query, Mutation, CtxT, S>
GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S>
where
S: ScalarValue + 'a,
for<'b> &'b S: ScalarRefValue<'b>,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
{
pub fn new(context_factory: CtxFactory, query: Query, mutation: Mutation) -> Self {
GraphQLHandler {
context_factory: context_factory,
root_node: RootNode::new(query, mutation),
}
}
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
let url_query_string = req
.get_mut::<UrlEncodedQuery>()
.map_err(GraphQLIronError::Url)?;
let input_query = parse_url_param(url_query_string.remove("query"))?
.ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
let variables = parse_variable_param(url_query_string.remove("variables"))?;
Ok(GraphQLBatchRequest::Single(http::GraphQLRequest::new(
input_query,
operation_name,
variables,
)))
}
fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLBatchRequest<S>> {
let mut request_payload = String::new();
itry!(req.body.read_to_string(&mut request_payload));
Ok(
serde_json::from_str::<GraphQLBatchRequest<S>>(request_payload.as_str())
.map_err(GraphQLIronError::Serde)?,
)
}
fn execute(&self, context: &CtxT, request: GraphQLBatchRequest<S>) -> IronResult<Response> {
let response = request.execute(&self.root_node, context);
let content_type = "application/json".parse::<Mime>().unwrap();
let json = serde_json::to_string_pretty(&response).unwrap();
let status = if response.is_ok() {
status::Ok
} else {
status::BadRequest
};
Ok(Response::with((content_type, status, json)))
}
}
impl GraphiQLHandler {
pub fn new(graphql_url: &str) -> GraphiQLHandler {
GraphiQLHandler {
graphql_url: graphql_url.to_owned(),
}
}
}
impl PlaygroundHandler {
pub fn new(graphql_url: &str) -> PlaygroundHandler {
PlaygroundHandler {
graphql_url: graphql_url.to_owned(),
}
}
}
impl<'a, CtxFactory, Query, Mutation, CtxT, S> Handler
for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT, S>
where
S: ScalarValue + Sync + Send + 'static,
for<'b> &'b S: ScalarRefValue<'b>,
CtxFactory: Fn(&mut Request) -> IronResult<CtxT> + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
Mutation: GraphQLType<S, Context = CtxT, TypeInfo = ()> + Send + Sync + 'static,
'a: 'static,
{
fn handle(&self, mut req: &mut Request) -> IronResult<Response> {
let context = (self.context_factory)(req)?;
let graphql_request = match req.method {
method::Get => self.handle_get(&mut req)?,
method::Post => self.handle_post(&mut req)?,
_ => return Ok(Response::with(status::MethodNotAllowed)),
};
self.execute(&context, graphql_request)
}
}
impl Handler for GraphiQLHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> {
let content_type = "text/html; charset=utf-8".parse::<Mime>().unwrap();
Ok(Response::with((
content_type,
status::Ok,
juniper::graphiql::graphiql_source(&self.graphql_url),
)))
}
}
impl Handler for PlaygroundHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> {
let content_type = "text/html; charset=utf-8".parse::<Mime>().unwrap();
Ok(Response::with((
content_type,
status::Ok,
juniper::http::playground::playground_source(&self.graphql_url),
)))
}
}
#[derive(Debug)]
enum GraphQLIronError {
Serde(SerdeError),
Url(UrlDecodingError),
InvalidData(&'static str),
}
impl fmt::Display for GraphQLIronError {
fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GraphQLIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f),
GraphQLIronError::Url(ref err) => fmt::Display::fmt(err, &mut f),
GraphQLIronError::InvalidData(err) => fmt::Display::fmt(err, &mut f),
}
}
}
impl Error for GraphQLIronError {
fn description(&self) -> &str {
match *self {
GraphQLIronError::Serde(ref err) => err.description(),
GraphQLIronError::Url(ref err) => err.description(),
GraphQLIronError::InvalidData(err) => err,
}
}
fn cause(&self) -> Option<&dyn Error> {
match *self {
GraphQLIronError::Serde(ref err) => Some(err),
GraphQLIronError::Url(ref err) => Some(err),
GraphQLIronError::InvalidData(_) => None,
}
}
}
impl From<GraphQLIronError> for IronError {
fn from(err: GraphQLIronError) -> IronError {
let message = format!("{}", err);
IronError::new(err, (status::BadRequest, message))
}
}
#[cfg(test)]
mod tests {
use super::*;
use iron::{Handler, Headers, Url};
use iron_test::{request, response};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use juniper::{
http::tests as http_tests,
tests::{model::Database, schema::Query},
EmptyMutation,
};
use super::GraphQLHandler;
const QUERY_ENCODE_SET: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');
fn fixup_url(url: &str) -> String {
let url = Url::parse(&format!("http://localhost:3000{}", url)).expect("url to parse");
let path: String = url
.path()
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("/");
format!(
"http://localhost:3000{}?{}",
path,
utf8_percent_encode(url.query().unwrap_or(""), QUERY_ENCODE_SET)
)
}
struct TestIronIntegration;
impl http_tests::HTTPIntegration for TestIronIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse {
let result = request::get(&fixup_url(url), Headers::new(), &make_handler());
match result {
Ok(response) => make_test_response(response),
Err(e) => make_test_error_response(e),
}
}
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
let result = request::post(&fixup_url(url), Headers::new(), body, &make_handler());
match result {
Ok(response) => make_test_response(response),
Err(e) => make_test_error_response(e),
}
}
}
#[test]
fn test_iron_integration() {
let integration = TestIronIntegration;
http_tests::run_http_test_suite(&integration);
}
fn context_factory(_: &mut Request) -> IronResult<Database> {
Ok(Database::new())
}
fn make_test_error_response(_: IronError) -> http_tests::TestResponse {
http_tests::TestResponse {
status_code: 400,
body: None,
content_type: "application/json".to_string(),
}
}
fn make_test_response(response: Response) -> http_tests::TestResponse {
let status_code = response
.status
.expect("No status code returned from handler")
.to_u16() as i32;
let content_type = String::from_utf8(
response
.headers
.get_raw("content-type")
.expect("No content type header from handler")[0]
.clone(),
)
.expect("Content-type header invalid UTF-8");
let body = response::extract_body_to_string(response);
http_tests::TestResponse {
status_code: status_code,
body: Some(body),
content_type: content_type,
}
}
fn make_handler() -> Box<dyn Handler> {
Box::new(GraphQLHandler::new(
context_factory,
Query,
EmptyMutation::<Database>::new(),
))
}
}