Extract general HTTP/JSON functions from Iron integration

This commit is contained in:
Magnus Hallin 2017-06-14 15:50:14 +02:00
parent cac5dd17c3
commit a7a6778b18
5 changed files with 237 additions and 271 deletions

64
src/graphiql.rs Normal file
View file

@ -0,0 +1,64 @@
pub fn graphiql_source(graphql_endpoint_url: &str) -> String {
let stylesheet_source = r#"
<style>
html, body, #app {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
"#;
let fetcher_source = r#"
<script>
function graphQLFetcher(params) {
return fetch(GRAPHQL_URL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
}).then(function (response) {
return response.text();
}).then(function (body) {
try {
return JSON.parse(body);
} catch (error) {
return body;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
}),
document.querySelector('#app'));
</script>
"#;
format!(r#"
<!DOCTYPE html>
<html>
<head>
<title>GraphQL</title>
{stylesheet_source}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.8.1/graphiql.css">
</head>
<body>
<div id="app"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.1/fetch.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.8.1/graphiql.js"></script>
<script>var GRAPHQL_URL = '{graphql_url}';</script>
{fetcher_source}
</body>
</html>
"#,
graphql_url = graphql_endpoint_url,
stylesheet_source = stylesheet_source,
fetcher_source = fetcher_source)
}

92
src/http.rs Normal file
View file

@ -0,0 +1,92 @@
use serde::ser;
use serde::ser::SerializeMap;
use ::{GraphQLError, Value, Variables, GraphQLType, RootNode};
use ast::InputValue;
use executor::ExecutionError;
/// The expected structure of the decoded JSON Document for either Post or Get requests.
#[derive(Deserialize)]
pub struct GraphQLRequest {
query: String,
#[serde(rename = "operationName")]
operation_name: Option<String>,
variables: Option<InputValue>
}
impl GraphQLRequest {
fn operation_name(&self) -> Option<&str> {
self.operation_name.as_ref().map(|oper_name| &**oper_name)
}
fn variables(&self) -> Variables {
self.variables.as_ref().and_then(|iv| {
iv.to_object_value().map(|o| {
o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect()
})
}).unwrap_or_default()
}
pub fn new(query: String, operation_name: Option<String>, variables: Option<InputValue>) -> GraphQLRequest {
GraphQLRequest {
query: query,
operation_name: operation_name,
variables: variables,
}
}
pub fn execute<'a, CtxT, QueryT, MutationT>(
&'a self,
root_node: &RootNode<QueryT, MutationT>,
context: &CtxT,
)
-> GraphQLResponse<'a>
where QueryT: GraphQLType<Context=CtxT>,
MutationT: GraphQLType<Context=CtxT>,
{
GraphQLResponse(::execute(
&self.query,
self.operation_name(),
root_node,
&self.variables(),
context,
))
}
}
pub struct GraphQLResponse<'a>(Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>);
impl<'a> GraphQLResponse<'a> {
pub fn is_ok(&self) -> bool {
self.0.is_ok()
}
}
impl<'a> ser::Serialize for GraphQLResponse<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: ser::Serializer,
{
match self.0 {
Ok((ref res, ref err)) => {
let mut map = try!(serializer.serialize_map(None));
try!(map.serialize_key("data"));
try!(map.serialize_value(res));
if !err.is_empty() {
try!(map.serialize_key("errors"));
try!(map.serialize_value(err));
}
map.end()
},
Err(ref err) => {
let mut map = try!(serializer.serialize_map(Some(1)));
try!(map.serialize_key("errors"));
try!(map.serialize_value(err));
map.end()
},
}
}
}

View file

@ -7,17 +7,14 @@ use iron::method;
use urlencoded::{UrlEncodedQuery, UrlDecodingError}; use urlencoded::{UrlEncodedQuery, UrlDecodingError};
use std::io::Read; use std::io::Read;
use std::io::Error as IoError;
use std::io::ErrorKind;
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::boxed::Box;
use serde_json; use serde_json;
use serde_json::error::Error as SerdeError; use serde_json::error::Error as SerdeError;
use ::{InputValue, GraphQLType, RootNode, execute}; use ::{InputValue, GraphQLType, RootNode};
use super::serde::{WrappedGraphQLResult, GraphQLQuery}; use ::http;
/// Handler that executes GraphQL queries in the given schema /// Handler that executes GraphQL queries in the given schema
/// ///
@ -45,45 +42,29 @@ pub struct GraphiQLHandler {
} }
/// Get queries are allowed to repeat the same key more than once. fn get_single_value<T>(mut values: Vec<T>) -> IronResult<T> {
fn check_for_repeat_keys(params: &Vec<String>) -> Result<(), IronError> { if values.len() == 1 {
if params.len() > 1 { Ok(values.remove(0))
let error = IronError::new(
Box::new(GraphQlIronError::IO(IoError::new(ErrorKind::InvalidData,
"Was able parse a query string \
but a duplicate uri key was \
found."))),
(status::BadRequest, "Duplicate uri key was found."));
Err(error)
} }
else { else {
Ok(()) Err(GraphQLIronError::InvalidData("Duplicate URL query parameter").into())
} }
} }
fn parse_url_param(param: Option<Vec<String>>) -> Result<Option<String>, IronError> { fn parse_url_param(params: Option<Vec<String>>) -> IronResult<Option<String>> {
if let Some(values) = param { if let Some(values) = params {
check_for_repeat_keys(&values)?; get_single_value(values).map(Some)
Ok(Some(values[0].to_owned()))
} }
else { else {
Ok(None) Ok(None)
} }
} }
fn parse_variable_param(param: Option<Vec<String>>) -> Result<Option<InputValue>, IronError> { fn parse_variable_param(params: Option<Vec<String>>) -> IronResult<Option<InputValue>> {
if let Some(values) = param { if let Some(values) = params {
check_for_repeat_keys(&values)?; Ok(serde_json::from_str::<InputValue>(get_single_value(values)?.as_ref())
match serde_json::from_str::<InputValue>(values[0].as_ref()) { .map(Some)
Ok(input_values) => { .map_err(GraphQLIronError::Serde)?)
Ok(Some(input_values))
}
Err(err) => {
Err(IronError::new(
Box::new(GraphQlIronError::Serde(err)),
(status::BadRequest, "No JSON object was decoded.")))
}
}
} }
else { else {
Ok(None) Ok(None)
@ -112,61 +93,35 @@ impl<'a, CtxFactory, Query, Mutation, CtxT>
} }
fn handle_get(&self, req: &mut Request) -> IronResult<GraphQLQuery> { fn handle_get(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> {
match req.get_mut::<UrlEncodedQuery>() { let url_query_string = req.get_mut::<UrlEncodedQuery>()
Ok(ref mut query_string) => { .map_err(|e| GraphQLIronError::Url(e))?;
let input_query = parse_url_param(query_string.remove("query").to_owned())?;
if let Some(query) = input_query { let input_query = parse_url_param(url_query_string.remove("query"))?
let operation_name = .ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
parse_url_param(query_string.remove("operationName"))?; let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
let input_variables = let variables = parse_variable_param(url_query_string.remove("variables"))?;
parse_variable_param(query_string.remove("variables"))?;
Ok(GraphQLQuery::new(query,operation_name,input_variables)) Ok(http::GraphQLRequest::new(input_query, operation_name, variables))
} else {
Err(IronError::new(
Box::new(GraphQlIronError::IO(IoError::new(ErrorKind::InvalidData,
"No query key was found in \
the Get request."))),
(status::BadRequest, "No query was provided.")))
}
}
Err(err) => {
Err(IronError::new(
Box::new(GraphQlIronError::Url(err)),
(status::BadRequest, "No JSON object was decoded.")))
}
}
} }
fn handle_post(&self, req: &mut Request) -> IronResult<GraphQLQuery> { fn handle_post(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> {
let mut request_payload = String::new(); let mut request_payload = String::new();
itry!(req.body.read_to_string(&mut request_payload)); itry!(req.body.read_to_string(&mut request_payload));
let graphql_query = serde_json::from_str::<GraphQLQuery>(request_payload.as_str()).map_err(|err|{
IronError::new( Ok(serde_json::from_str::<http::GraphQLRequest>(request_payload.as_str())
Box::new(GraphQlIronError::Serde(err)), .map_err(|err| GraphQLIronError::Serde(err))?)
(status::BadRequest, "No JSON object was decoded."))
});
graphql_query
} }
fn respond(&self, req: &mut Request, graphql: GraphQLQuery) -> IronResult<Response> { fn execute(&self, context: &CtxT, request: http::GraphQLRequest) -> IronResult<Response> {
let context = (self.context_factory)(req); let response = request.execute(
let variables = graphql.variables();
let result = execute(graphql.query(),
graphql.operation_name(),
&self.root_node, &self.root_node,
&variables, context,
&context); );
let content_type = "application/json".parse::<Mime>().unwrap(); let content_type = "application/json".parse::<Mime>().unwrap();
if result.is_ok() {
let response = WrappedGraphQLResult::new(result);
let json = serde_json::to_string_pretty(&response).unwrap(); let json = serde_json::to_string_pretty(&response).unwrap();
Ok(Response::with((content_type, status::Ok, json))) let status = if response.is_ok() { status::Ok } else { status::BadRequest };
} else { Ok(Response::with((content_type, status, json)))
let response = WrappedGraphQLResult::new(result);
let json = serde_json::to_string_pretty(&response).unwrap();
Ok(Response::with((content_type, status::BadRequest, json)))
}
} }
} }
@ -191,17 +146,15 @@ impl<'a, CtxFactory, Query, Mutation, CtxT>
Mutation: GraphQLType<Context=CtxT> + Send + Sync + 'static, 'a: 'static, Mutation: GraphQLType<Context=CtxT> + Send + Sync + 'static, 'a: 'static,
{ {
fn handle(&self, mut req: &mut Request) -> IronResult<Response> { fn handle(&self, mut req: &mut Request) -> IronResult<Response> {
match req.method { let context = (self.context_factory)(req);
method::Get => {
let graphql_query = self.handle_get(&mut req)?; let graphql_request = match req.method {
self.respond(&mut req, graphql_query) method::Get => self.handle_get(&mut req)?,
} method::Post => self.handle_post(&mut req)?,
method::Post => { _ => return Ok(Response::with((status::MethodNotAllowed)))
let graphql_query = self.handle_post(&mut req)?; };
self.respond(&mut req, graphql_query)
}, self.execute(&context, graphql_request)
_ => Ok(Response::with((status::MethodNotAllowed)))
}
} }
} }
@ -209,121 +162,54 @@ impl Handler for GraphiQLHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> { fn handle(&self, _: &mut Request) -> IronResult<Response> {
let content_type = "text/html".parse::<Mime>().unwrap(); let content_type = "text/html".parse::<Mime>().unwrap();
let stylesheet_source = r#" Ok(Response::with((
<style> content_type,
html, body, #app { status::Ok,
height: 100%; ::graphiql::graphiql_source(&self.graphql_url),
margin: 0; )))
overflow: hidden;
width: 100%;
}
</style>
"#;
let fetcher_source = r#"
<script>
function graphQLFetcher(params) {
return fetch(GRAPHQL_URL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params)
}).then(function (response) {
return response.text();
}).then(function (body) {
try {
return JSON.parse(body);
} catch (error) {
return body;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
}),
document.querySelector('#app'));
</script>
"#;
let source = format!(r#"
<!DOCTYPE html>
<html>
<head>
<title>GraphQL</title>
{stylesheet_source}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.8.1/graphiql.css">
</head>
<body>
<div id="app"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.1/fetch.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.8.1/graphiql.js"></script>
<script>var GRAPHQL_URL = '{graphql_url}';</script>
{fetcher_source}
</body>
</html>
"#,
graphql_url = self.graphql_url,
stylesheet_source = stylesheet_source,
fetcher_source = fetcher_source);
Ok(Response::with((content_type, status::Ok, source)))
} }
} }
/// A general error allowing the developer to see the underlying issue.
#[derive(Debug)] #[derive(Debug)]
pub enum GraphQlIronError { enum GraphQLIronError {
///Captures any errors that were caused by Serde.
Serde(SerdeError), Serde(SerdeError),
/// Captures any error related the IO. Url(UrlDecodingError),
IO(IoError), InvalidData(&'static str),
/// Captures any error related to Url Decoding,
Url(UrlDecodingError)
} }
impl fmt::Display for GraphQlIronError { impl fmt::Display for GraphQLIronError {
fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
GraphQlIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f), GraphQLIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f),
GraphQlIronError::IO(ref err) => fmt::Display::fmt(err, &mut f), GraphQLIronError::Url(ref err) => fmt::Display::fmt(err, &mut f),
GraphQlIronError::Url(ref err) => fmt::Display::fmt(err, &mut f), GraphQLIronError::InvalidData(ref err) => fmt::Display::fmt(err, &mut f),
} }
} }
} }
impl Error for GraphQlIronError { impl Error for GraphQLIronError {
fn description(&self) -> &str { fn description(&self) -> &str {
match *self { match *self {
GraphQlIronError::Serde(ref err) => { GraphQLIronError::Serde(ref err) => err.description(),
err.description() GraphQLIronError::Url(ref err) => err.description(),
}, GraphQLIronError::InvalidData(ref err) => err,
GraphQlIronError::IO(ref err) => {
err.description()
}
GraphQlIronError::Url(ref err) => {
err.description()
}
} }
} }
fn cause(&self) -> Option<&Error> { fn cause(&self) -> Option<&Error> {
match *self { match *self {
GraphQlIronError::Serde(ref err) => { GraphQLIronError::Serde(ref err) => Some(err),
err.cause() GraphQLIronError::Url(ref err) => Some(err),
} GraphQLIronError::InvalidData(_) => None,
GraphQlIronError::IO(ref err) => {
err.cause()
}
GraphQlIronError::Url(ref err) => {
err.cause()
} }
} }
} }
impl From<GraphQLIronError> for IronError {
fn from(err: GraphQLIronError) -> IronError {
let message = format!("{}", err);
IronError::new(err, (status::BadRequest, message))
}
} }

View file

@ -3,7 +3,7 @@ use serde::ser::SerializeMap;
use std::fmt; use std::fmt;
use std::collections::HashMap; use std::collections::HashMap;
use ::{GraphQLError, Value, Variables}; use ::{GraphQLError, Value};
use ast::InputValue; use ast::InputValue;
use executor::ExecutionError; use executor::ExecutionError;
use parser::{ParseError, Spanning, SourcePosition}; use parser::{ParseError, Spanning, SourcePosition};
@ -223,81 +223,3 @@ impl ser::Serialize for Value {
} }
} }
} }
/// The expected structure of the decoded JSON Document for either Post or Get requests.
#[derive(Deserialize)]
pub struct GraphQLQuery {
query: String,
#[serde(rename = "operationName")]
operation_name: Option<String>,
variables: Option<InputValue>
}
impl GraphQLQuery {
pub fn new(query: String,
operation_name: Option<String>,
variables: Option<InputValue>
) -> GraphQLQuery {
GraphQLQuery {
query: query,
operation_name: operation_name,
variables: variables
}
}
pub fn query(&self) -> &str {
self.query.as_str()
}
pub fn operation_name(&self) -> Option<&str> {
self.operation_name.as_ref().map(|oper_name| &**oper_name)
}
pub fn variables(&self) -> Variables {
self.variables.as_ref().and_then(|iv| {
iv.to_object_value().map(|o| {
o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect()
})
}).unwrap_or_default()
}
}
pub struct WrappedGraphQLResult<'a>(Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>);
impl<'a> WrappedGraphQLResult<'a> {
pub fn new(result: Result<(Value, Vec<ExecutionError>),
GraphQLError<'a>>
) -> WrappedGraphQLResult<'a> {
WrappedGraphQLResult(result)
}
}
impl<'a> ser::Serialize for WrappedGraphQLResult<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: ser::Serializer,
{
match self.0 {
Ok((ref res, ref err)) => {
let mut map = try!(serializer.serialize_map(None));
try!(map.serialize_key("data"));
try!(map.serialize_value(res));
if !err.is_empty() {
try!(map.serialize_key("errors"));
try!(map.serialize_value(err));
}
map.end()
},
Err(ref err) => {
let mut map = try!(serializer.serialize_map(Some(1)));
try!(map.serialize_key("errors"));
try!(map.serialize_value(err));
map.end()
},
}
}
}

View file

@ -203,6 +203,8 @@ mod schema;
mod validation; mod validation;
mod executor; mod executor;
mod integrations; mod integrations;
pub mod graphiql;
pub mod http;
#[macro_use] mod result_ext; #[macro_use] mod result_ext;
#[cfg(all(test, not(feature="expose-test-schema")))] mod tests; #[cfg(all(test, not(feature="expose-test-schema")))] mod tests;