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::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<QueryT, MutationT, CtxT>(
pub fn execute_validated_query<'a, QueryT, MutationT, CtxT>(
document: Document,
operation_name: Option<&str>,
root_node: &RootNode<CtxT, QueryT, MutationT>,
variables: &HashMap<String, InputValue>,
context: &CtxT
)
-> (Value, Vec<ExecutionError>)
-> Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>
where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>
{
let mut fragments = vec![];
let mut operation = None;
@ -232,7 +242,7 @@ pub fn execute_validated_query<QueryT, MutationT, CtxT>(
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<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 value;
@ -269,7 +283,7 @@ pub fn execute_validated_query<QueryT, MutationT, CtxT>(
errors.sort();
(value, errors)
Ok((value, errors))
}
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 enums;
mod directives;
mod executor;

View file

@ -226,6 +226,9 @@ pub use schema::meta;
pub enum GraphQLError<'a> {
ParseError(Spanning<ParseError<'a>>),
ValidationError(Vec<RuleError>),
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<Spanning<ParseError<'a>>> 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![

View file

@ -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<T, CtxT>(
&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<Vec<Directive>>, vars: &HashMap<String, Input
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.
pub fn as_list_value(&self) -> Option<&Vec<Value>> {
match *self {