From c5552419780a8d4fccf5c34c5c24bc976829fd4e Mon Sep 17 00:00:00 2001 From: Magnus Hallin Date: Sat, 12 Nov 2016 17:49:04 +0100 Subject: [PATCH] Add executor tests, fix a bunch of bugs --- src/executor.rs | 26 ++- src/executor_tests/executor.rs | 332 +++++++++++++++++++++++++++++++++ src/executor_tests/mod.rs | 1 + src/lib.rs | 11 +- src/types/base.rs | 45 ++++- src/value.rs | 8 + 6 files changed, 414 insertions(+), 9 deletions(-) create mode 100644 src/executor_tests/executor.rs diff --git a/src/executor.rs b/src/executor.rs index ddb6ed50..764027ab 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::marker::PhantomData; +use ::GraphQLError; use ast::{InputValue, ToInputValue, Document, Selection, Fragment, Definition, Type, FromInputValue, OperationType}; use value::Value; use parser::SourcePosition; @@ -198,6 +199,15 @@ impl<'a> FieldPath<'a> { } impl ExecutionError { + #[doc(hidden)] + pub fn new(location: SourcePosition, path: &[&str], message: &str) -> ExecutionError { + ExecutionError { + location: location, + path: path.iter().map(|s| (*s).to_owned()).collect(), + message: message.to_owned(), + } + } + /// The error message pub fn message(&self) -> &str { &self.message @@ -214,16 +224,16 @@ impl ExecutionError { } } -pub fn execute_validated_query( +pub fn execute_validated_query<'a, QueryT, MutationT, CtxT>( document: Document, operation_name: Option<&str>, root_node: &RootNode, variables: &HashMap, context: &CtxT ) - -> (Value, Vec) + -> Result<(Value, Vec), GraphQLError<'a>> where QueryT: GraphQLType, - MutationT: GraphQLType, + MutationT: GraphQLType { let mut fragments = vec![]; let mut operation = None; @@ -232,7 +242,7 @@ pub fn execute_validated_query( match def { Definition::Operation(op) => { if operation_name.is_none() && operation.is_some() { - panic!("Must provide operation name if query contains multiple operations"); + return Err(GraphQLError::MultipleOperationsProvided); } let move_op = operation_name.is_none() @@ -246,7 +256,11 @@ pub fn execute_validated_query( }; } - let op = operation.expect("Could not find operation to execute"); + let op = match operation { + Some(op) => op, + None => return Err(GraphQLError::UnknownOperationName), + }; + let mut errors = Vec::new(); let value; @@ -269,7 +283,7 @@ pub fn execute_validated_query( errors.sort(); - (value, errors) + Ok((value, errors)) } impl Registry { diff --git a/src/executor_tests/executor.rs b/src/executor_tests/executor.rs new file mode 100644 index 00000000..6d416579 --- /dev/null +++ b/src/executor_tests/executor.rs @@ -0,0 +1,332 @@ +mod field_execution { + use value::Value; + use ast::InputValue; + use schema::model::RootNode; + + struct DataType; + struct DeepDataType; + + graphql_object!(DataType: () |&self| { + field a() -> &str { "Apple" } + field b() -> &str { "Banana" } + field c() -> &str { "Cookie" } + field d() -> &str { "Donut" } + field e() -> &str { "Egg" } + field f() -> &str { "Fish" } + + field pic(size: Option) -> String { + format!("Pic of size: {}", size.unwrap_or(50)) + } + + field deep() -> DeepDataType { + DeepDataType + } + }); + + graphql_object!(DeepDataType: () |&self| { + field a() -> &str { "Already Been Done" } + field b() -> &str { "Boring" } + field c() -> &[Option<&str>] { &[Some("Contrived"), None, Some("Confusing")] } + + field deeper() -> &[Option] { &[Some(DataType), None, Some(DataType) ] } + }); + + #[test] + fn test() { + let schema = RootNode::new(DataType, ()); + let doc = r" + query Example($size: Int) { + a, + b, + x: c + ...c + f + ...on DataType { + pic(size: $size) + } + deep { + a + b + c + deeper { + a + b + } + } + } + + fragment c on DataType { + d + e + }"; + + let vars = vec![ + ("size".to_owned(), InputValue::int(100)) + ].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("a", Value::string("Apple")), + ("b", Value::string("Banana")), + ("x", Value::string("Cookie")), + ("d", Value::string("Donut")), + ("e", Value::string("Egg")), + ("f", Value::string("Fish")), + ("pic", Value::string("Pic of size: 100")), + ("deep", Value::object(vec![ + ("a", Value::string("Already Been Done")), + ("b", Value::string("Boring")), + ("c", Value::list(vec![ + Value::string("Contrived"), + Value::null(), + Value::string("Confusing"), + ])), + ("deeper", Value::list(vec![ + Value::object(vec![ + ("a", Value::string("Apple")), + ("b", Value::string("Banana")), + ].into_iter().collect()), + Value::null(), + Value::object(vec![ + ("a", Value::string("Apple")), + ("b", Value::string("Banana")), + ].into_iter().collect()), + ])), + ].into_iter().collect())), + ].into_iter().collect())); + } +} + + +mod merge_parallel_fragments { + use value::Value; + use schema::model::RootNode; + + struct Type; + + graphql_object!(Type: () |&self| { + field a() -> &str { "Apple" } + field b() -> &str { "Banana" } + field c() -> &str { "Cherry" } + field deep() -> Type { Type } + }); + + #[test] + fn test() { + let schema = RootNode::new(Type, ()); + let doc = r" + { a, ...FragOne, ...FragTwo } + fragment FragOne on Type { + b + deep { b, deeper: deep { b } } + } + fragment FragTwo on Type { + c + deep { c, deeper: deep { c } } + }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("a", Value::string("Apple")), + ("b", Value::string("Banana")), + ("c", Value::string("Cherry")), + ("deep", Value::object(vec![ + ("b", Value::string("Banana")), + ("c", Value::string("Cherry")), + ("deeper", Value::object(vec![ + ("b", Value::string("Banana")), + ("c", Value::string("Cherry")), + ].into_iter().collect())), + ].into_iter().collect())), + ].into_iter().collect())); + } +} + +mod threads_context_correctly { + use value::Value; + use schema::model::RootNode; + + struct Schema; + + graphql_object!(Schema: String |&self| { + field a(&mut executor) -> String { executor.context().clone() } + }); + + #[test] + fn test() { + let schema = RootNode::new(Schema, ()); + let doc = r"{ a }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &"Context value".to_owned()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("a", Value::string("Context value")), + ].into_iter().collect())); + } +} + +mod nulls_out_errors { + use value::Value; + use schema::model::RootNode; + use executor::{ExecutionError, FieldResult}; + use parser::SourcePosition; + + struct Schema; + + graphql_object!(Schema: () |&self| { + field sync() -> FieldResult<&str> { Ok("sync") } + field sync_error() -> FieldResult<&str> { Err("Error for syncError".to_owned()) } + }); + + #[test] + fn test() { + let schema = RootNode::new(Schema, ()); + let doc = r"{ sync, syncError }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .expect("Execution failed"); + + println!("Result: {:?}", result); + + assert_eq!( + result, + Value::object(vec![ + ("sync", Value::string("sync")), + ("syncError", Value::null()), + ].into_iter().collect())); + + assert_eq!( + errs, + vec![ + ExecutionError::new( + SourcePosition::new(8, 0, 8), + &["syncError"], + "Error for syncError", + ), + ]); + } +} + +mod named_operations { + use value::Value; + use schema::model::RootNode; + use ::GraphQLError; + + struct Schema; + + graphql_object!(Schema: () |&self| { + field a() -> &str { "b" } + }); + + #[test] + fn uses_inline_operation_if_no_name_provided() { + let schema = RootNode::new(Schema, ()); + let doc = r"{ a }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + assert_eq!( + result, + Value::object(vec![ + ("a", Value::string("b")), + ].into_iter().collect())); + } + + #[test] + fn uses_only_named_operation() { + let schema = RootNode::new(Schema, ()); + let doc = r"query Example { a }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + assert_eq!( + result, + Value::object(vec![ + ("a", Value::string("b")), + ].into_iter().collect())); + } + + #[test] + fn uses_named_operation_if_name_provided() { + let schema = RootNode::new(Schema, ()); + let doc = r"query Example { first: a } query OtherExample { second: a }"; + + let vars = vec![].into_iter().collect(); + + let (result, errs) = ::execute(doc, Some("OtherExample"), &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + assert_eq!( + result, + Value::object(vec![ + ("second", Value::string("b")), + ].into_iter().collect())); + } + + #[test] + fn error_if_multiple_operations_provided_but_no_name() { + let schema = RootNode::new(Schema, ()); + let doc = r"query Example { first: a } query OtherExample { second: a }"; + + let vars = vec![].into_iter().collect(); + + let err = ::execute(doc, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(err, GraphQLError::MultipleOperationsProvided); + } + + #[test] + fn error_if_unknown_operation_name_provided() { + let schema = RootNode::new(Schema, ()); + let doc = r"query Example { first: a } query OtherExample { second: a }"; + + let vars = vec![].into_iter().collect(); + + let err = ::execute(doc, Some("UnknownExample"), &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(err, GraphQLError::UnknownOperationName); + } +} diff --git a/src/executor_tests/mod.rs b/src/executor_tests/mod.rs index 74b8ac9a..33440ba8 100644 --- a/src/executor_tests/mod.rs +++ b/src/executor_tests/mod.rs @@ -2,3 +2,4 @@ mod introspection; mod variables; mod enums; mod directives; +mod executor; diff --git a/src/lib.rs b/src/lib.rs index 8facda63..37d8165d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,6 +226,9 @@ pub use schema::meta; pub enum GraphQLError<'a> { ParseError(Spanning>), ValidationError(Vec), + NoOperationProvided, + MultipleOperationsProvided, + UnknownOperationName, } /// Execute a query in a provided schema @@ -260,7 +263,7 @@ pub fn execute<'a, CtxT, QueryT, MutationT>( } } - Ok(execute_validated_query(document, operation_name, root_node, variables, context)) + execute_validated_query(document, operation_name, root_node, variables, context) } impl<'a> From>> for GraphQLError<'a> { @@ -274,6 +277,12 @@ impl<'a> ToJson for GraphQLError<'a> { let errs = 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()), }; Json::Object(vec![ diff --git a/src/types/base.rs b/src/types/base.rs index 5d0f73f9..ab2576cf 100644 --- a/src/types/base.rs +++ b/src/types/base.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::hash_map::Entry; use ast::{InputValue, Selection, Directive, FromInputValue}; use value::Value; @@ -324,8 +325,11 @@ fn resolve_selection_set_into( &mut sub_exec); match field_result { - Ok(v) => { result.insert(response_name.clone(), v); } - Err(e) => { sub_exec.push_error(e, start_pos); } + Ok(v) => merge_key_into(result, response_name.clone(), v), + Err(e) => { + sub_exec.push_error(e, start_pos); + result.insert(response_name.clone(), Value::null()); + } } }, Selection::FragmentSpread(Spanning { item: spread, .. }) => { @@ -408,3 +412,40 @@ fn is_excluded(directives: &Option>, vars: &HashMap, + response_name: String, + value: Value, +) { + match result.entry(response_name) { + Entry::Occupied(mut e) => { + println!("Merging object at '{}'", e.key()); + match (e.get_mut().as_mut_object_value(), value) { + (Some(dest_obj), Value::Object(src_obj)) => { + merge_maps(dest_obj, src_obj); + }, + _ => { + println!("Not merging object/object - this is bad :("); + } + } + }, + Entry::Vacant(e) => { + e.insert(value); + }, + } +} + +fn merge_maps( + dest: &mut HashMap, + src: HashMap, +) { + for (key, value) in src { + if dest.contains_key(&key) { + merge_key_into(dest, key, value); + } + else { + dest.insert(key, value); + } + } +} diff --git a/src/value.rs b/src/value.rs index 7df16b3f..67e5d01b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -75,6 +75,14 @@ impl Value { } } + /// Mutable view into the underlying object value, if present. + pub fn as_mut_object_value(&mut self) -> Option<&mut HashMap> { + match *self { + Value::Object(ref mut o) => Some(o), + _ => None, + } + } + /// View the underlying list value, if present. pub fn as_list_value(&self) -> Option<&Vec> { match *self {