From 41112d0b6d50115a67066af7fcbb84616382ad81 Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 30 Apr 2017 20:07:36 -0600 Subject: [PATCH 01/11] Drop support for rustc-serialization and use serde by default. --- Cargo.toml | 7 +- examples/server.rs | 2 +- src/integrations/rustc_serialize.rs | 114 ---------------------------- 3 files changed, 5 insertions(+), 118 deletions(-) delete mode 100644 src/integrations/rustc_serialize.rs diff --git a/Cargo.toml b/Cargo.toml index da46c251..44b35bb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,15 +16,16 @@ harness = false path = "benches/bench.rs" [features] -default = ["rustc-serialize"] +default = ["serde", "serde_json"] nightly = [] -iron-handlers = ["iron", "rustc-serialize"] +iron-handlers = ["iron", "serde", "serde_json"] expose-test-schema = [] [dependencies] -rustc-serialize = { version = "^0.3.19", optional = true } iron = { version = "^0.5.1", optional = true } serde = { version = "^0.9.1", optional = true } +serde_json = {version ="^0.9.1", optional = true } + [dev-dependencies] iron = "^0.5.1" diff --git a/examples/server.rs b/examples/server.rs index e4ef3cbe..9aa31107 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -1,7 +1,7 @@ extern crate iron; extern crate mount; extern crate logger; -extern crate rustc_serialize; +extern crate serde; #[macro_use] extern crate juniper; use std::env; diff --git a/src/integrations/rustc_serialize.rs b/src/integrations/rustc_serialize.rs deleted file mode 100644 index 23bfdcc8..00000000 --- a/src/integrations/rustc_serialize.rs +++ /dev/null @@ -1,114 +0,0 @@ -use rustc_serialize::json::{ToJson, Json}; - -use ::{GraphQLError, Value}; -use ast::InputValue; -use executor::ExecutionError; -use parser::{ParseError, Spanning, SourcePosition}; -use validation::RuleError; - -fn parse_error_to_json(err: &Spanning<ParseError>) -> Json { - Json::Array(vec![ - Json::Object(vec![ - ("message".to_owned(), format!("{}", err.item).to_json()), - ("locations".to_owned(), vec![ - Json::Object(vec![ - ("line".to_owned(), (err.start.line() + 1).to_json()), - ("column".to_owned(), (err.start.column() + 1).to_json()) - ].into_iter().collect()), - ].to_json()), - ].into_iter().collect()), - ]) -} - -impl ToJson for ExecutionError { - fn to_json(&self) -> Json { - Json::Object(vec![ - ("message".to_owned(), self.message().to_json()), - ("locations".to_owned(), vec![self.location().clone()].to_json()), - ("path".to_owned(), self.path().to_json()), - ].into_iter().collect()) - } -} - -impl<'a> ToJson for GraphQLError<'a> { - fn to_json(&self) -> Json { - match *self { - GraphQLError::ParseError(ref err) => parse_error_to_json(err), - GraphQLError::ValidationError(ref errs) => errs.to_json(), - GraphQLError::MultipleOperationsProvided => Json::String( - "Must provide operation name if query contains multiple operations".to_owned()), - GraphQLError::NoOperationProvided => Json::String( - "Must provide an operation".to_owned()), - GraphQLError::UnknownOperationName => Json::String( - "Unknown operation".to_owned()), - } - } -} - -impl ToJson for InputValue { - fn to_json(&self) -> Json { - match *self { - InputValue::Null | InputValue::Variable(_) => Json::Null, - InputValue::Int(i) => Json::I64(i), - InputValue::Float(f) => Json::F64(f), - InputValue::String(ref s) | InputValue::Enum(ref s) => Json::String(s.clone()), - InputValue::Boolean(b) => Json::Boolean(b), - InputValue::List(ref l) => Json::Array(l.iter().map(|x| x.item.to_json()).collect()), - InputValue::Object(ref o) => Json::Object(o.iter().map(|&(ref k, ref v)| (k.item.clone(), v.item.to_json())).collect()), - } - } -} - -impl InputValue { - /// Convert a `Json` structure into an `InputValue`. - /// - /// This consumes the JSON instance. - /// - /// Notes: - /// * No enums or variables will be produced by this method. - /// * All lists and objects will be unlocated - pub fn from_json(json: Json) -> InputValue { - match json { - Json::I64(i) => InputValue::int(i), - Json::U64(u) => InputValue::float(u as f64), - Json::F64(f) => InputValue::float(f), - Json::String(s) => InputValue::string(s), - Json::Boolean(b) => InputValue::boolean(b), - Json::Array(a) => InputValue::list(a.into_iter().map(InputValue::from_json).collect()), - Json::Object(o) => InputValue::object(o.into_iter().map(|(k, v)| (k, InputValue::from_json(v))).collect()), - Json::Null => InputValue::null(), - } - } -} - -impl ToJson for RuleError { - fn to_json(&self) -> Json { - Json::Object(vec![ - ("message".to_owned(), self.message().to_json()), - ("locations".to_owned(), self.locations().to_json()), - ].into_iter().collect()) - } -} - -impl ToJson for SourcePosition { - fn to_json(&self) -> Json { - Json::Object(vec![ - ("line".to_owned(), (self.line() + 1).to_json()), - ("column".to_owned(), (self.column() + 1).to_json()), - ].into_iter().collect()) - } -} - -impl ToJson for Value { - fn to_json(&self) -> Json { - match *self { - Value::Null => Json::Null, - Value::Int(i) => Json::I64(i), - Value::Float(f) => Json::F64(f), - Value::String(ref s) => Json::String(s.clone()), - Value::Boolean(b) => Json::Boolean(b), - Value::List(ref l) => Json::Array(l.iter().map(|x| x.to_json()).collect()), - Value::Object(ref o) => Json::Object(o.iter().map(|(k,v)| (k.clone(), v.to_json())).collect()), - } - } -} From 9adc0c4479094266c4093b4062ef47710b161236 Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 30 Apr 2017 20:09:10 -0600 Subject: [PATCH 02/11] Removed rustc-serialization as dependency and updated the docs that referenced rustc. --- src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fb94eaac..919f3887 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ schema, as well as an optional integration into the [Iron framework][2]. It tries to keep the number of dynamic operations to a minimum, and give you as the schema developer the control of the query execution path. -Juniper only depends on `rustc-serialize` by default, making it lightweight and +Juniper only depends on `serde` and `serde_json` by default, making it lightweight and easy to drop into any project. Through the `iron-handlers` feature, it also depends on Iron. @@ -185,13 +185,11 @@ built-in [GraphiQL][6] handler included. #![cfg_attr(feature="nightly", feature(test))] #![warn(missing_docs)] - -#[cfg(feature="rustc-serialize")] extern crate rustc_serialize; -#[cfg(feature="serde")] extern crate serde; - #[cfg(feature="nightly")] extern crate test; #[cfg(feature="iron-handlers")] #[macro_use(itry, iexpect)] extern crate iron; #[cfg(test)] extern crate iron_test; +extern crate serde; +extern crate serde_json; #[macro_use] mod macros; mod ast; From 54cfbeb010107f08c8066402ce241b0bf598859a Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 30 Apr 2017 20:20:08 -0600 Subject: [PATCH 03/11] - Replaced rustc-serialization logic with Serde. - Added a GraphQlIronError - Changed execute from lib.rs to execute_query --- src/integrations/iron_handlers.rs | 163 +++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 45 deletions(-) diff --git a/src/integrations/iron_handlers.rs b/src/integrations/iron_handlers.rs index 1101aefd..a4f363eb 100644 --- a/src/integrations/iron_handlers.rs +++ b/src/integrations/iron_handlers.rs @@ -7,11 +7,19 @@ use iron::status; use iron::method; use iron::url::Url; -use std::collections::BTreeMap; +use std::io::Read; +use std::io::Error as IoError; +use std::io::ErrorKind; +use std::collections::HashMap; +use std::error::Error; +use std::fmt; +use std::boxed::Box; -use rustc_serialize::json::{ToJson, Json}; +use serde_json; +use serde_json::Value as Json; +use serde_json::error::Error as SerdeError; -use ::{InputValue, GraphQLType, RootNode, Variables, execute}; +use ::{InputValue, GraphQLType, RootNode, Variables, execute as execute_query}; /// Handler that executes GraphQL queries in the given schema /// @@ -77,59 +85,74 @@ impl<'a, CtxFactory, Query, Mutation, CtxT> } fn handle_post(&self, req: &mut Request) -> IronResult<Response> { - let json_data = itry!(Json::from_reader(&mut req.body)); - - let json_obj = match json_data { - Json::Object(o) => o, - _ => return Ok(Response::with((status::BadRequest, "No JSON object was decoded"))), - }; - - let mut query = None; - let mut variables = Variables::new(); - - for (k, v) in json_obj { - if k == "query" { - query = v.as_string().map(|s| s.to_owned()); + let mut request_payload = String::new(); + itry!(req.body.read_to_string(&mut request_payload)); + let json_data = + match serde_json::from_str::<Json>(&*request_payload) { + Ok(json) => json, + Err(err) => { + let error = IronError::new( + Box::new(GraphQlIronError::Serde(err)), + (status::BadRequest, "No JSON object was decoded.")); + return Err(error) + } + }; + match json_data { + Json::Object(json_obj) => { + let mut query = None; + let mut variables = Variables::new(); + for (k, v) in json_obj { + if k == "query" { + query = v.as_str().map(|query| query.to_owned()); + } + else if k == "variables" { + variables = InputValue::from_json(v).to_object_value() + .map(|o| o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect()) + .unwrap_or_default(); + } + } + let query = iexpect!(query); + self.execute(req, &query, &variables) } - else if k == "variables" { - variables = InputValue::from_json(v).to_object_value() - .map(|o| o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect()) - .unwrap_or_default(); + _ => { + let error = IronError::new( + Box::new(GraphQlIronError::IO(IoError::new(ErrorKind::InvalidData, + "Was able parse a JSON item but it\ + was not an object as expected."))), + (status::BadRequest, "No JSON object was decoded.")); + Err(error) } } - - let query = iexpect!(query); - - self.execute(req, &query, &variables) } fn execute(&self, req: &mut Request, query: &str, variables: &Variables) -> IronResult<Response> { let context = (self.context_factory)(req); - let result = execute(query, None, &self.root_node, variables, &context); - + let result = execute_query(query, None, &self.root_node, variables, &context); let content_type = "application/json".parse::<Mime>().unwrap(); - let mut map = BTreeMap::new(); + let mut map = HashMap::new(); match result { Ok((result, errors)) => { - map.insert("data".to_owned(), result.to_json()); + let response_data = serde_json::to_value(result) + .expect("Failed to convert response data to JSON."); + map.insert("data".to_owned(), response_data); if !errors.is_empty() { - map.insert("errors".to_owned(), errors.to_json()); + let response_data = serde_json::to_value(errors) + .expect("Failed to convert the errors to JSON."); + map.insert("errors".to_owned(), response_data); } - - let data = Json::Object(map); - let json = data.pretty(); - - Ok(Response::with((content_type, status::Ok, json.to_string()))) + let data = serde_json::to_value(map).expect("Failed to convert response to JSON"); + let json = serde_json::to_string_pretty(&data).expect("Failed to convert response to JSON."); + Ok(Response::with((content_type, status::Ok, json))) } - Err(err) => { - map.insert("errors".to_owned(), err.to_json()); - - let data = Json::Object(map); - let json = data.pretty(); - + let response_data = serde_json::to_value(err) + .expect("Failed to convert error data to JSON."); + map.insert("errors".to_owned(), response_data); + let data = serde_json::to_value(map).expect("Failed to convert response to JSON"); + let json = serde_json::to_string_pretty(&data) + .expect("Failed to convert response to JSON"); Ok(Response::with((content_type, status::BadRequest, json.to_string()))) } } @@ -236,11 +259,61 @@ impl Handler for GraphiQLHandler { } } +/// A general error allowing the developer to see the underlying issue. +pub enum GraphQlIronError { + ///Captures any errors that were caused by Serde. + Serde(SerdeError), + /// Captures any error related the IO. + IO(IoError) +} + +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::IO(ref err) => fmt::Display::fmt(err, &mut f), + } + } +} + +impl fmt::Debug for GraphQlIronError { + fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result { + match *self { + GraphQlIronError::Serde(ref err) => fmt::Debug::fmt(err, &mut f), + GraphQlIronError::IO(ref err) => fmt::Debug::fmt(err, &mut f), + } + } +} + +impl Error for GraphQlIronError { + fn description(&self) -> &str { + match *self { + GraphQlIronError::Serde(ref err) => { + err.description() + }, + GraphQlIronError::IO(ref err) => { + err.description() + } + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + GraphQlIronError::Serde(ref err) => { + err.cause() + } + GraphQlIronError::IO(ref err) => { + err.cause() + } + } + } +} + #[cfg(test)] mod tests { - use rustc_serialize::json::Json; - + use serde_json::Value as Json; + use serde_json; use iron::prelude::*; use iron::status; use iron::headers; @@ -267,7 +340,7 @@ mod tests { fn unwrap_json_response(resp: Response) -> Json { let result = response::extract_body_to_string(resp); - Json::from_str(&result).expect("Could not parse JSON object") + serde_json::from_str::<Json>(&result).expect("Could not parse JSON object") } #[test] @@ -286,7 +359,7 @@ mod tests { assert_eq!( json, - Json::from_str(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) + serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) .expect("Invalid JSON constant in test")); } @@ -307,7 +380,7 @@ mod tests { assert_eq!( json, - Json::from_str(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) + serde_json::from_str::<Json>(r#"{"data": {"hero": {"name": "R2-D2"}}}"#) .expect("Invalid JSON constant in test")); } From 494628a7c7132e39f55f3be3d9ac2148220c2a5c Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 30 Apr 2017 20:20:41 -0600 Subject: [PATCH 04/11] Removed rustc-serialization as a dependency. --- src/integrations/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs index f597709d..02768f18 100644 --- a/src/integrations/mod.rs +++ b/src/integrations/mod.rs @@ -1,3 +1,2 @@ #[cfg(feature="iron-handlers")] pub mod iron_handlers; -#[cfg(feature="rustc-serialize")] pub mod rustc_serialize; -#[cfg(feature="serde")] pub mod serde; +pub mod serde; From 1e334becda092577a2caecfb08b2eb03d2919ff4 Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 30 Apr 2017 20:22:04 -0600 Subject: [PATCH 05/11] Implemented from_json for InputValue. --- src/integrations/serde.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/integrations/serde.rs b/src/integrations/serde.rs index e8662949..6d298787 100644 --- a/src/integrations/serde.rs +++ b/src/integrations/serde.rs @@ -1,13 +1,42 @@ use serde::{de, ser}; use serde::ser::SerializeMap; -use std::collections::HashMap; use std::fmt; +use std::collections::HashMap; use ::{GraphQLError, Value}; use ast::InputValue; use executor::ExecutionError; use parser::{ParseError, Spanning, SourcePosition}; use validation::RuleError; +use serde_json::Value as Json; + + +impl InputValue { + /// Converts serde_json::Value to juniper::InputValue + pub fn from_json(json: Json) -> InputValue { + match json { + Json::Number(num) => { + if let Some(number) = num.as_i64() { + InputValue::int(number) + } + else if let Some(number) = num.as_f64() { + InputValue::float(number) + } + else if let Some(number) = num.as_u64() { + InputValue::float(number as f64) + } + else { + panic!("Invalid number data type was found."); + } + } + Json::String(s) => InputValue::string(s), + Json::Bool(b) => InputValue::boolean(b), + Json::Array(a) => InputValue::list(a.into_iter().map(InputValue::from_json).collect()), + Json::Object(o) => InputValue::object(o.into_iter().map(|(k, v)| (k, InputValue::from_json(v))).collect()), + Json::Null => InputValue::null(), + } + } +} impl ser::Serialize for ExecutionError { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> From 6c4b329848099bbcd39f786df5bdcd55eede7268 Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 14 May 2017 19:30:49 -0600 Subject: [PATCH 06/11] Added serde_derive and urlencoded as dependecies. --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 44b35bb7..9600688a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,15 @@ path = "benches/bench.rs" [features] default = ["serde", "serde_json"] nightly = [] -iron-handlers = ["iron", "serde", "serde_json"] +iron-handlers = ["iron", "default", "serde_derive", "urlencoded"] expose-test-schema = [] [dependencies] iron = { version = "^0.5.1", optional = true } serde = { version = "^0.9.1", optional = true } serde_json = {version ="^0.9.1", optional = true } +serde_derive = {version="^0.9.1", optional = true } +urlencoded = {version="0.5", optional=true} [dev-dependencies] From e890e9b4bded6844fea0343cbf162169684953fc Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 14 May 2017 19:33:37 -0600 Subject: [PATCH 07/11] Removed from_json, leveraged the already implemented serialization trait and added a serialization implementation for the graphql result. --- src/integrations/serde.rs | 111 ++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/src/integrations/serde.rs b/src/integrations/serde.rs index 6d298787..c4e09269 100644 --- a/src/integrations/serde.rs +++ b/src/integrations/serde.rs @@ -3,41 +3,13 @@ use serde::ser::SerializeMap; use std::fmt; use std::collections::HashMap; -use ::{GraphQLError, Value}; +use ::{GraphQLError, Value, Variables}; use ast::InputValue; use executor::ExecutionError; use parser::{ParseError, Spanning, SourcePosition}; use validation::RuleError; -use serde_json::Value as Json; -impl InputValue { - /// Converts serde_json::Value to juniper::InputValue - pub fn from_json(json: Json) -> InputValue { - match json { - Json::Number(num) => { - if let Some(number) = num.as_i64() { - InputValue::int(number) - } - else if let Some(number) = num.as_f64() { - InputValue::float(number) - } - else if let Some(number) = num.as_u64() { - InputValue::float(number as f64) - } - else { - panic!("Invalid number data type was found."); - } - } - Json::String(s) => InputValue::string(s), - Json::Bool(b) => InputValue::boolean(b), - Json::Array(a) => InputValue::list(a.into_iter().map(InputValue::from_json).collect()), - Json::Object(o) => InputValue::object(o.into_iter().map(|(k, v)| (k, InputValue::from_json(v))).collect()), - Json::Null => InputValue::null(), - } - } -} - impl ser::Serialize for ExecutionError { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: ser::Serializer, @@ -242,3 +214,84 @@ impl ser::Serialize for Value { } } } + +/// The expected structure of the decoded JSON Document for either Post or Get requests. +#[cfg(feature="iron-handlers")] +#[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() + } + +} + + +#[cfg(feature="iron-handlers")] +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() + }, + } + } +} From 998faec4cb098a68a9b49b94f5e599b1ed24d4ed Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 14 May 2017 19:34:34 -0600 Subject: [PATCH 08/11] Removed iexpect macro, added urlencoded and serde_derive dependencies. --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 919f3887..0e0bc631 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,10 +186,12 @@ built-in [GraphiQL][6] handler included. #![cfg_attr(feature="nightly", feature(test))] #![warn(missing_docs)] #[cfg(feature="nightly")] extern crate test; -#[cfg(feature="iron-handlers")] #[macro_use(itry, iexpect)] extern crate iron; +#[cfg(feature="iron-handlers")] #[macro_use(itry)] extern crate iron; +#[cfg(feature="iron-handlers")] extern crate urlencoded; #[cfg(test)] extern crate iron_test; extern crate serde; extern crate serde_json; +#[cfg(feature="iron-handlers")] #[macro_use] extern crate serde_derive; #[macro_use] mod macros; mod ast; From d3b1433748288802f386343e69eed8073296a9bd Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 14 May 2017 19:37:34 -0600 Subject: [PATCH 09/11] Added more support for get requests and the get and post handlers leverage the GraphQlQuery struct. Added a couple more tests for get requests. --- src/integrations/iron_handlers.rs | 290 +++++++++++++++++++----------- 1 file changed, 187 insertions(+), 103 deletions(-) diff --git a/src/integrations/iron_handlers.rs b/src/integrations/iron_handlers.rs index a4f363eb..87f9776a 100644 --- a/src/integrations/iron_handlers.rs +++ b/src/integrations/iron_handlers.rs @@ -1,25 +1,23 @@ //! Optional handlers for the Iron framework. Requires the `iron-handlers` feature enabled. - use iron::prelude::*; use iron::middleware::Handler; use iron::mime::Mime; use iron::status; use iron::method; -use iron::url::Url; +use urlencoded::{UrlEncodedQuery, UrlDecodingError}; use std::io::Read; use std::io::Error as IoError; use std::io::ErrorKind; -use std::collections::HashMap; use std::error::Error; use std::fmt; use std::boxed::Box; use serde_json; -use serde_json::Value as Json; use serde_json::error::Error as SerdeError; -use ::{InputValue, GraphQLType, RootNode, Variables, execute as execute_query}; +use ::{InputValue, GraphQLType, RootNode, execute}; +use super::serde::{WrappedGraphQLResult, GraphQlQuery}; /// Handler that executes GraphQL queries in the given schema /// @@ -46,6 +44,53 @@ pub struct GraphiQLHandler { graphql_url: String, } + +/// Get queries are allowed to repeat the same key more than once. +fn check_for_repeat_keys(params: &Vec<String>) -> Result<(), IronError> { + if params.len() > 1 { + 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 { + Ok(()) + } +} + +fn parse_url_param(param: Option<Vec<String>>) -> Result<Option<String>, IronError> { + if let Some(values) = param { + check_for_repeat_keys(&values)?; + Ok(Some(values[0].to_owned())) + } + else { + Ok(None) + } +} + +fn parse_variable_param(param: Option<Vec<String>>) -> Result<Option<InputValue>, IronError> { + if let Some(values) = param { + check_for_repeat_keys(&values)?; + match serde_json::from_str::<InputValue>(values[0].as_ref()) { + Ok(input_values) => { + Ok(Some(input_values)) + } + Err(err) => { + Err(IronError::new( + Box::new(GraphQlIronError::Serde(err)), + (status::BadRequest, "No JSON object was decoded."))) + } + } + } + else { + Ok(None) + } +} + + impl<'a, CtxFactory, Query, Mutation, CtxT> GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT> where CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static, @@ -67,96 +112,62 @@ impl<'a, CtxFactory, Query, Mutation, CtxT> } - fn handle_get(&self, req: &mut Request) -> IronResult<Response> { - let url: Url = req.url.clone().into(); - - let mut query = None; - let variables = Variables::new(); - - for (k, v) in url.query_pairs() { - if k == "query" { - query = Some(v.into_owned()); - } - } - - let query = iexpect!(query); - - self.execute(req, &query, &variables) - } - - fn handle_post(&self, req: &mut Request) -> IronResult<Response> { - let mut request_payload = String::new(); - itry!(req.body.read_to_string(&mut request_payload)); - let json_data = - match serde_json::from_str::<Json>(&*request_payload) { - Ok(json) => json, - Err(err) => { - let error = IronError::new( - Box::new(GraphQlIronError::Serde(err)), - (status::BadRequest, "No JSON object was decoded.")); - return Err(error) + fn handle_get(&self, req: &mut Request) -> IronResult<GraphQlQuery> { + match req.get_mut::<UrlEncodedQuery>() { + Ok(ref mut query_string) => { + let input_query = parse_url_param(query_string.remove("query").to_owned())?; + if let Some(query) = input_query { + let operation_name = + parse_url_param(query_string.remove("operationName"))?; + let input_variables = + parse_variable_param(query_string.remove("variables"))?; + Ok(GraphQlQuery::new(query,operation_name,input_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."))) } - }; - match json_data { - Json::Object(json_obj) => { - let mut query = None; - let mut variables = Variables::new(); - for (k, v) in json_obj { - if k == "query" { - query = v.as_str().map(|query| query.to_owned()); - } - else if k == "variables" { - variables = InputValue::from_json(v).to_object_value() - .map(|o| o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect()) - .unwrap_or_default(); - } - } - let query = iexpect!(query); - self.execute(req, &query, &variables) - } - _ => { - let error = IronError::new( - Box::new(GraphQlIronError::IO(IoError::new(ErrorKind::InvalidData, - "Was able parse a JSON item but it\ - was not an object as expected."))), - (status::BadRequest, "No JSON object was decoded.")); - Err(error) - } - } - } - - fn execute(&self, req: &mut Request, query: &str, variables: &Variables) -> IronResult<Response> { - let context = (self.context_factory)(req); - let result = execute_query(query, None, &self.root_node, variables, &context); - let content_type = "application/json".parse::<Mime>().unwrap(); - let mut map = HashMap::new(); - - match result { - Ok((result, errors)) => { - let response_data = serde_json::to_value(result) - .expect("Failed to convert response data to JSON."); - map.insert("data".to_owned(), response_data); - - if !errors.is_empty() { - let response_data = serde_json::to_value(errors) - .expect("Failed to convert the errors to JSON."); - map.insert("errors".to_owned(), response_data); - } - let data = serde_json::to_value(map).expect("Failed to convert response to JSON"); - let json = serde_json::to_string_pretty(&data).expect("Failed to convert response to JSON."); - Ok(Response::with((content_type, status::Ok, json))) } Err(err) => { - let response_data = serde_json::to_value(err) - .expect("Failed to convert error data to JSON."); - map.insert("errors".to_owned(), response_data); - let data = serde_json::to_value(map).expect("Failed to convert response to JSON"); - let json = serde_json::to_string_pretty(&data) - .expect("Failed to convert response to JSON"); - Ok(Response::with((content_type, status::BadRequest, json.to_string()))) + Err(IronError::new( + Box::new(GraphQlIronError::Url(err)), + (status::BadRequest, "No JSON object was decoded."))) } } } + + fn handle_post(&self, req: &mut Request) -> IronResult<GraphQlQuery> { + let mut request_payload = String::new(); + 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( + Box::new(GraphQlIronError::Serde(err)), + (status::BadRequest, "No JSON object was decoded.")) + }); + graphql_query + } + + fn respond(&self, req: &mut Request, graphql: GraphQlQuery) -> IronResult<Response> { + let context = (self.context_factory)(req); + let variables = graphql.variables(); + let result = execute(graphql.query(), + graphql.operation_name(), + &self.root_node, + &variables, + &context); + 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(); + Ok(Response::with((content_type, status::Ok, json))) + } else { + let response = WrappedGraphQLResult::new(result); + let json = serde_json::to_string_pretty(&response).unwrap(); + Ok(Response::with((content_type, status::BadRequest, json))) + } + } } impl GraphiQLHandler { @@ -179,10 +190,16 @@ impl<'a, CtxFactory, Query, Mutation, CtxT> Query: GraphQLType<Context=CtxT> + Send + Sync + 'static, Mutation: GraphQLType<Context=CtxT> + Send + Sync + 'static, 'a: 'static, { - fn handle(&self, req: &mut Request) -> IronResult<Response> { + fn handle(&self, mut req: &mut Request) -> IronResult<Response> { match req.method { - method::Get => self.handle_get(req), - method::Post => self.handle_post(req), + method::Get => { + let graphql_query = self.handle_get(&mut req)?; + self.respond(&mut req, graphql_query) + } + method::Post => { + let graphql_query = self.handle_post(&mut req)?; + self.respond(&mut req, graphql_query) + }, _ => Ok(Response::with((status::MethodNotAllowed))) } } @@ -202,7 +219,6 @@ impl Handler for GraphiQLHandler { } </style> "#; - let fetcher_source = r#" <script> function graphQLFetcher(params) { @@ -260,11 +276,14 @@ impl Handler for GraphiQLHandler { } /// A general error allowing the developer to see the underlying issue. +#[derive(Debug)] pub enum GraphQlIronError { ///Captures any errors that were caused by Serde. Serde(SerdeError), /// Captures any error related the IO. - IO(IoError) + IO(IoError), + /// Captures any error related to Url Decoding, + Url(UrlDecodingError) } impl fmt::Display for GraphQlIronError { @@ -272,15 +291,7 @@ impl fmt::Display for GraphQlIronError { match *self { GraphQlIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f), GraphQlIronError::IO(ref err) => fmt::Display::fmt(err, &mut f), - } - } -} - -impl fmt::Debug for GraphQlIronError { - fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result { - match *self { - GraphQlIronError::Serde(ref err) => fmt::Debug::fmt(err, &mut f), - GraphQlIronError::IO(ref err) => fmt::Debug::fmt(err, &mut f), + GraphQlIronError::Url(ref err) => fmt::Display::fmt(err, &mut f), } } } @@ -294,6 +305,9 @@ impl Error for GraphQlIronError { GraphQlIronError::IO(ref err) => { err.description() } + GraphQlIronError::Url(ref err) => { + err.description() + } } } @@ -305,6 +319,9 @@ impl Error for GraphQlIronError { GraphQlIronError::IO(ref err) => { err.cause() } + GraphQlIronError::Url(ref err) => { + err.cause() + } } } } @@ -363,6 +380,73 @@ mod tests { .expect("Invalid JSON constant in test")); } + #[test] + fn test_encoded_get() { + let response = request::get( + "http://localhost:3000/?query=query%20{%20%20%20human(id:%20\"1000\")%20{%20%20%20%20%20id,%20%20%20%20%20name,%20%20%20%20%20appearsIn,%20%20%20%20%20homePlanet%20%20%20}%20}", + Headers::new(), + &make_handler()) + .expect("Unexpected IronError"); + + assert_eq!(response.status, Some(status::Ok)); + assert_eq!(response.headers.get::<headers::ContentType>(), + Some(&headers::ContentType::json())); + + let json = unwrap_json_response(response); + + assert_eq!( + json, + serde_json::from_str::<Json>(r#"{ + "data": { + "human": { + "appearsIn": [ + "NEW_HOPE", + "EMPIRE", + "JEDI" + ], + "homePlanet": "Tatooine", + "name": "Luke Skywalker", + "id": "1000" + } + } + }"#) + .expect("Invalid JSON constant in test")); + } + + #[test] + fn test_get_with_variables() { + let response = request::get( + "http://localhost:3000/?query=query($id:%20String!)%20{%20%20%20human(id:%20$id)%20{%20%20%20%20%20id,%20%20%20%20%20name,%20%20%20%20%20appearsIn,%20%20%20%20%20homePlanet%20%20%20}%20}&variables={%20%20%20\"id\":%20%20\"1000\"%20}", + Headers::new(), + &make_handler()) + .expect("Unexpected IronError"); + + assert_eq!(response.status, Some(status::Ok)); + assert_eq!(response.headers.get::<headers::ContentType>(), + Some(&headers::ContentType::json())); + + let json = unwrap_json_response(response); + + assert_eq!( + json, + serde_json::from_str::<Json>(r#"{ + "data": { + "human": { + "appearsIn": [ + "NEW_HOPE", + "EMPIRE", + "JEDI" + ], + "homePlanet": "Tatooine", + "name": "Luke Skywalker", + "id": "1000" + } + } + }"#) + .expect("Invalid JSON constant in test")); + } + + #[test] fn test_simple_post() { let response = request::post( From ba15579531b41119320d7d906bcde3d80448f20e Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Sun, 14 May 2017 20:10:46 -0600 Subject: [PATCH 10/11] Added conditional compliation logic. --- src/integrations/serde.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/integrations/serde.rs b/src/integrations/serde.rs index c4e09269..3595b9b5 100644 --- a/src/integrations/serde.rs +++ b/src/integrations/serde.rs @@ -3,7 +3,9 @@ use serde::ser::SerializeMap; use std::fmt; use std::collections::HashMap; -use ::{GraphQLError, Value, Variables}; +use ::{GraphQLError, Value}; +#[cfg(feature="iron-handlers")] +use ::Variables; use ast::InputValue; use executor::ExecutionError; use parser::{ParseError, Spanning, SourcePosition}; @@ -225,6 +227,7 @@ pub struct GraphQlQuery { variables: Option<InputValue> } +#[cfg(feature="iron-handlers")] impl GraphQlQuery { pub fn new(query: String, @@ -260,6 +263,7 @@ impl GraphQlQuery { #[cfg(feature="iron-handlers")] pub struct WrappedGraphQLResult<'a>(Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>); +#[cfg(feature="iron-handlers")] impl<'a> WrappedGraphQLResult<'a> { pub fn new(result: Result<(Value, Vec<ExecutionError>), GraphQLError<'a>> @@ -268,6 +272,7 @@ impl<'a> WrappedGraphQLResult<'a> { } } +#[cfg(feature="iron-handlers")] impl<'a> ser::Serialize for WrappedGraphQLResult<'a> { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: ser::Serializer, From 80de43e551b74377021b6b342f1cd7f5f3cbbe26 Mon Sep 17 00:00:00 2001 From: rpiper <asterisktheserver@gmail.com> Date: Mon, 22 May 2017 08:24:31 -0600 Subject: [PATCH 11/11] Drop support for rust 1.13 and 1.14 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 67aad4ab..55bd8f51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ rust: - nightly # The two most recent stable releases before "stable" - - 1.13.0 - - 1.14.0 + - 1.15.0 + - 1.16.0 matrix: allow_failures: