Add executor tests, fix a bunch of bugs

This commit is contained in:
Magnus Hallin 2016-11-12 17:49:04 +01:00
parent 251f957a7f
commit c555241978
6 changed files with 414 additions and 9 deletions

View file

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::marker::PhantomData; use std::marker::PhantomData;
use ::GraphQLError;
use ast::{InputValue, ToInputValue, Document, Selection, Fragment, Definition, Type, FromInputValue, OperationType}; use ast::{InputValue, ToInputValue, Document, Selection, Fragment, Definition, Type, FromInputValue, OperationType};
use value::Value; use value::Value;
use parser::SourcePosition; use parser::SourcePosition;
@ -198,6 +199,15 @@ impl<'a> FieldPath<'a> {
} }
impl ExecutionError { 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 /// The error message
pub fn message(&self) -> &str { pub fn message(&self) -> &str {
&self.message &self.message
@ -214,16 +224,16 @@ impl ExecutionError {
} }
} }
pub fn execute_validated_query<QueryT, MutationT, CtxT>( pub fn execute_validated_query<'a, QueryT, MutationT, CtxT>(
document: Document, document: Document,
operation_name: Option<&str>, operation_name: Option<&str>,
root_node: &RootNode<CtxT, QueryT, MutationT>, root_node: &RootNode<CtxT, QueryT, MutationT>,
variables: &HashMap<String, InputValue>, variables: &HashMap<String, InputValue>,
context: &CtxT context: &CtxT
) )
-> (Value, Vec<ExecutionError>) -> Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>
where QueryT: GraphQLType<CtxT>, where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>, MutationT: GraphQLType<CtxT>
{ {
let mut fragments = vec![]; let mut fragments = vec![];
let mut operation = None; let mut operation = None;
@ -232,7 +242,7 @@ pub fn execute_validated_query<QueryT, MutationT, CtxT>(
match def { match def {
Definition::Operation(op) => { Definition::Operation(op) => {
if operation_name.is_none() && operation.is_some() { 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() let move_op = operation_name.is_none()
@ -246,7 +256,11 @@ pub fn execute_validated_query<QueryT, MutationT, CtxT>(
}; };
} }
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 mut errors = Vec::new();
let value; let value;
@ -269,7 +283,7 @@ pub fn execute_validated_query<QueryT, MutationT, CtxT>(
errors.sort(); errors.sort();
(value, errors) Ok((value, errors))
} }
impl<CtxT> Registry<CtxT> { impl<CtxT> Registry<CtxT> {

View file

@ -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<i64>) -> 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<DataType>] { &[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);
}
}

View file

@ -2,3 +2,4 @@ mod introspection;
mod variables; mod variables;
mod enums; mod enums;
mod directives; mod directives;
mod executor;

View file

@ -226,6 +226,9 @@ pub use schema::meta;
pub enum GraphQLError<'a> { pub enum GraphQLError<'a> {
ParseError(Spanning<ParseError<'a>>), ParseError(Spanning<ParseError<'a>>),
ValidationError(Vec<RuleError>), ValidationError(Vec<RuleError>),
NoOperationProvided,
MultipleOperationsProvided,
UnknownOperationName,
} }
/// Execute a query in a provided schema /// 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<Spanning<ParseError<'a>>> for GraphQLError<'a> { impl<'a> From<Spanning<ParseError<'a>>> for GraphQLError<'a> {
@ -274,6 +277,12 @@ impl<'a> ToJson for GraphQLError<'a> {
let errs = match *self { let errs = match *self {
GraphQLError::ParseError(ref err) => parse_error_to_json(err), GraphQLError::ParseError(ref err) => parse_error_to_json(err),
GraphQLError::ValidationError(ref errs) => errs.to_json(), 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![ Json::Object(vec![

View file

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::hash_map::Entry;
use ast::{InputValue, Selection, Directive, FromInputValue}; use ast::{InputValue, Selection, Directive, FromInputValue};
use value::Value; use value::Value;
@ -324,8 +325,11 @@ fn resolve_selection_set_into<T, CtxT>(
&mut sub_exec); &mut sub_exec);
match field_result { match field_result {
Ok(v) => { result.insert(response_name.clone(), v); } Ok(v) => merge_key_into(result, response_name.clone(), v),
Err(e) => { sub_exec.push_error(e, start_pos); } Err(e) => {
sub_exec.push_error(e, start_pos);
result.insert(response_name.clone(), Value::null());
}
} }
}, },
Selection::FragmentSpread(Spanning { item: spread, .. }) => { Selection::FragmentSpread(Spanning { item: spread, .. }) => {
@ -408,3 +412,40 @@ fn is_excluded(directives: &Option<Vec<Directive>>, vars: &HashMap<String, Input
false false
} }
} }
fn merge_key_into(
result: &mut HashMap<String, Value>,
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<String, Value>,
src: HashMap<String, Value>,
) {
for (key, value) in src {
if dest.contains_key(&key) {
merge_key_into(dest, key, value);
}
else {
dest.insert(key, value);
}
}
}

View file

@ -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<String, Value>> {
match *self {
Value::Object(ref mut o) => Some(o),
_ => None,
}
}
/// View the underlying list value, if present. /// View the underlying list value, if present.
pub fn as_list_value(&self) -> Option<&Vec<Value>> { pub fn as_list_value(&self) -> Option<&Vec<Value>> {
match *self { match *self {