diff --git a/src/ast.rs b/src/ast.rs index 7c83033f..fa563341 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -289,7 +289,8 @@ impl InputValue { /// Resolve all variables to their values. pub fn into_const(self, vars: &HashMap<String, InputValue>) -> InputValue { match self { - InputValue::Variable(v) => vars[&v].clone(), + InputValue::Variable(v) => vars.get(&v) + .map_or_else(InputValue::null, Clone::clone), InputValue::List(l) => InputValue::List( l.into_iter().map(|s| s.map(|v| v.into_const(vars))).collect() ), diff --git a/src/executor_tests/mod.rs b/src/executor_tests/mod.rs index 481c5bd1..391693c2 100644 --- a/src/executor_tests/mod.rs +++ b/src/executor_tests/mod.rs @@ -1 +1,2 @@ mod introspection; +mod variables; diff --git a/src/executor_tests/variables.rs b/src/executor_tests/variables.rs new file mode 100644 index 00000000..76a930c7 --- /dev/null +++ b/src/executor_tests/variables.rs @@ -0,0 +1,737 @@ +use std::collections::HashMap; + +use value::Value; +use ast::InputValue; +use schema::model::RootNode; +use ::GraphQLError::ValidationError; +use validation::RuleError; +use parser::SourcePosition; + +#[derive(Debug)] +struct TestComplexScalar; + +struct TestType; + +graphql_scalar!(TestComplexScalar { + resolve(&self) -> Value { + Value::string("SerializedValue") + } + + from_input_value(v: &InputValue) -> Option<TestComplexScalar> { + if let Some(s) = v.as_string_value() { + if s == "SerializedValue" { + return Some(TestComplexScalar); + } + } + + None + } +}); + + +graphql_input_object!( + #[derive(Debug)] + struct TestInputObject { + a: Option<String>, + b: Option<Vec<Option<String>>>, + c: String, + d: Option<TestComplexScalar>, + } +); + +graphql_input_object!( + #[derive(Debug)] + struct TestNestedInputObject { + na: TestInputObject, + nb: String, + } +); + +graphql_object!(TestType: () |&self| { + field field_with_object_input(input: Option<TestInputObject>) -> String { + format!("{:?}", input) + } + + field field_with_nullable_string_input(input: Option<String>) -> String { + format!("{:?}", input) + } + + field field_with_non_nullable_string_input(input: String) -> String { + format!("{:?}", input) + } + + field field_with_default_argument_value(input = ("Hello World".to_owned()): String) -> String { + format!("{:?}", input) + } + + field field_with_nested_object_input(input: Option<TestNestedInputObject>) -> String { + format!("{:?}", input) + } + + field list(input: Option<Vec<Option<String>>>) -> String { + format!("{:?}", input) + } + + field nn_list(input: Vec<Option<String>>) -> String { + format!("{:?}", input) + } + + field list_nn(input: Option<Vec<String>>) -> String { + format!("{:?}", input) + } + + field nn_list_nn(input: Vec<String>) -> String { + format!("{:?}", input) + } +}); + +fn run_variable_query<F>(query: &str, vars: HashMap<String, InputValue>, f: F) + where F: Fn(&HashMap<String, Value>) -> () +{ + let schema = RootNode::new(TestType, ()); + + let (result, errs) = ::execute(query, None, &schema, &vars, &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + let obj = result.as_object_value().expect("Result is not an object"); + + f(obj); +} + +fn run_query<F>(query: &str, f: F) + where F: Fn(&HashMap<String, Value>) -> () +{ + run_variable_query(query, HashMap::new(), f); +} + +#[test] +fn inline_complex_input() { + run_query( + r#"{ fieldWithObjectInput(input: {a: "foo", b: ["bar"], c: "baz"}) }"#, + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + }); +} + +#[test] +fn inline_parse_single_value_to_list() { + run_query( + r#"{ fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"}) }"#, + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + }); +} + +#[test] +fn inline_runs_from_input_value_on_scalar() { + run_query( + r#"{ fieldWithObjectInput(input: {c: "baz", d: "SerializedValue"}) }"#, + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#))); + }); +} + +#[test] +fn variable_complex_input() { + run_variable_query( + r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::object(vec![ + ("a", InputValue::string("foo")), + ("b", InputValue::list(vec![InputValue::string("bar")])), + ("c", InputValue::string("baz")), + ].into_iter().collect())), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + }); +} + +#[test] +fn variable_parse_single_value_to_list() { + run_variable_query( + r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::object(vec![ + ("a", InputValue::string("foo")), + ("b", InputValue::string("bar")), + ("c", InputValue::string("baz")), + ].into_iter().collect())), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: Some("foo"), b: Some([Some("bar")]), c: "baz", d: None })"#))); + }); +} + +#[test] +fn variable_runs_from_input_value_on_scalar() { + run_variable_query( + r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::object(vec![ + ("c", InputValue::string("baz")), + ("d", InputValue::string("SerializedValue")), + ].into_iter().collect())), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithObjectInput"), + Some(&Value::string(r#"Some(TestInputObject { a: None, b: None, c: "baz", d: Some(TestComplexScalar) })"#))); + }); +} + +#[test] +fn variable_error_on_nested_non_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::object(vec![ + ("a", InputValue::string("foo")), + ("b", InputValue::string("bar")), + ("c", InputValue::null()), + ].into_iter().collect())) + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In field "c": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn variable_error_on_incorrect_type() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::string("foo bar")), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. Expected "TestInputObject", found not an object."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn variable_error_on_omit_non_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::object(vec![ + ("a", InputValue::string("foo")), + ("b", InputValue::string("bar")), + ].into_iter().collect())) + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In field "c": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn variable_multiple_errors_with_nesting() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestNestedInputObject) { fieldWithNestedObjectInput(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::object(vec![ + ("na", InputValue::object(vec![ + ("a", InputValue::string("foo")), + ].into_iter().collect())), + ].into_iter().collect())) + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In field "na": In field "c": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + RuleError::new( + r#"Variable "$input" got invalid value. In field "nb": Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn variable_error_on_additional_field() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestInputObject) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::object(vec![ + ("a", InputValue::string("foo")), + ("b", InputValue::string("bar")), + ("c", InputValue::string("baz")), + ("extra", InputValue::string("dog")), + ].into_iter().collect())) + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In field "extra": Unknown field."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn allow_nullable_inputs_to_be_omitted() { + run_query( + r#"{ fieldWithNullableStringInput }"#, + |result| { + assert_eq!( + result.get("fieldWithNullableStringInput"), + Some(&Value::string(r#"None"#))); + }); +} + +#[test] +fn allow_nullable_inputs_to_be_omitted_in_variable() { + run_query( + r#"query q($value: String) { fieldWithNullableStringInput(input: $value) }"#, + |result| { + assert_eq!( + result.get("fieldWithNullableStringInput"), + Some(&Value::string(r#"None"#))); + }); +} + +#[test] +fn allow_nullable_inputs_to_be_set_to_null_in_variable() { + run_variable_query( + r#"query q($value: String) { fieldWithNullableStringInput(input: $value) }"#, + vec![ + ("value".to_owned(), InputValue::null()), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithNullableStringInput"), + Some(&Value::string(r#"None"#))); + }); +} + +#[test] +fn allow_nullable_inputs_to_be_set_to_value_in_variable() { + run_variable_query( + r#"query q($value: String) { fieldWithNullableStringInput(input: $value) }"#, + vec![ + ("value".to_owned(), InputValue::string("a")), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithNullableStringInput"), + Some(&Value::string(r#"Some("a")"#))); + }); +} + +#[test] +fn allow_nullable_inputs_to_be_set_to_value_directly() { + run_query( + r#"{ fieldWithNullableStringInput(input: "a") }"#, + |result| { + assert_eq!( + result.get("fieldWithNullableStringInput"), + Some(&Value::string(r#"Some("a")"#))); + }); +} + +#[test] +fn does_not_allow_non_nullable_input_to_be_omitted_in_variable() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($value: String!) { fieldWithNonNullableStringInput(input: $value) }"#; + let vars = vec![ + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$value" of required type "String!" was not provided."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn does_not_allow_non_nullable_input_to_be_set_to_null_in_variable() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($value: String!) { fieldWithNonNullableStringInput(input: $value) }"#; + let vars = vec![ + ("value".to_owned(), InputValue::null()), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$value" of required type "String!" was not provided."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn allow_non_nullable_inputs_to_be_set_to_value_in_variable() { + run_variable_query( + r#"query q($value: String!) { fieldWithNonNullableStringInput(input: $value) }"#, + vec![ + ("value".to_owned(), InputValue::string("a")), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithNonNullableStringInput"), + Some(&Value::string(r#""a""#))); + }); +} + +#[test] +fn allow_non_nullable_inputs_to_be_set_to_value_directly() { + run_query( + r#"{ fieldWithNonNullableStringInput(input: "a") }"#, + |result| { + assert_eq!( + result.get("fieldWithNonNullableStringInput"), + Some(&Value::string(r#""a""#))); + }); +} + +#[test] +fn allow_lists_to_be_null() { + run_variable_query( + r#"query q($input: [String]) { list(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::null()), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("list"), + Some(&Value::string(r#"None"#))); + }); +} + +#[test] +fn allow_lists_to_contain_values() { + run_variable_query( + r#"query q($input: [String]) { list(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("list"), + Some(&Value::string(r#"Some([Some("A")])"#))); + }); +} + +#[test] +fn allow_lists_to_contain_null() { + run_variable_query( + r#"query q($input: [String]) { list(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::null(), + InputValue::string("B"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("list"), + Some(&Value::string(r#"Some([Some("A"), None, Some("B")])"#))); + }); +} + +#[test] +fn does_not_allow_non_null_lists_to_be_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: [String]!) { nnList(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::null()), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" of required type "[String]!" was not provided."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn allow_non_null_lists_to_contain_values() { + run_variable_query( + r#"query q($input: [String]!) { nnList(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("nnList"), + Some(&Value::string(r#"[Some("A")]"#))); + }); +} +#[test] +fn allow_non_null_lists_to_contain_null() { + run_variable_query( + r#"query q($input: [String]!) { nnList(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::null(), + InputValue::string("B"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("nnList"), + Some(&Value::string(r#"[Some("A"), None, Some("B")]"#))); + }); +} + +#[test] +fn allow_lists_of_non_null_to_be_null() { + run_variable_query( + r#"query q($input: [String!]) { listNn(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::null()), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("listNn"), + Some(&Value::string(r#"None"#))); + }); +} + +#[test] +fn allow_lists_of_non_null_to_contain_values() { + run_variable_query( + r#"query q($input: [String!]) { listNn(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("listNn"), + Some(&Value::string(r#"Some(["A"])"#))); + }); +} + +#[test] +fn does_not_allow_lists_of_non_null_to_contain_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: [String!]) { listNn(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::null(), + InputValue::string("B"), + ])), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn does_not_allow_non_null_lists_of_non_null_to_contain_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: [String!]!) { nnListNn(input: $input) }"#; + let vars = vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::null(), + InputValue::string("B"), + ])), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" got invalid value. In element #1: Expected "String!", found null."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn does_not_allow_non_null_lists_of_non_null_to_be_null() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: [String!]!) { nnListNn(input: $input) }"#; + let vars = vec![ + ("value".to_owned(), InputValue::null()), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" of required type "[String!]!" was not provided."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn allow_non_null_lists_of_non_null_to_contain_values() { + run_variable_query( + r#"query q($input: [String!]!) { nnListNn(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + ])), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("nnListNn"), + Some(&Value::string(r#"["A"]"#))); + }); +} + +#[test] +fn does_not_allow_invalid_types_to_be_used_as_values() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: TestType!) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("value".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::string("B"), + ])), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" expected value of type "TestType!" which cannot be used as an input type."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn does_not_allow_unknown_types_to_be_used_as_values() { + let schema = RootNode::new(TestType, ()); + + let query = r#"query q($input: UnknownType!) { fieldWithObjectInput(input: $input) }"#; + let vars = vec![ + ("value".to_owned(), InputValue::list(vec![ + InputValue::string("A"), + InputValue::string("B"), + ])), + ].into_iter().collect(); + + let error = ::execute(query, None, &schema, &vars, &()) + .unwrap_err(); + + assert_eq!(error, ValidationError(vec![ + RuleError::new( + r#"Variable "$input" expected value of type "UnknownType!" which cannot be used as an input type."#, + &[SourcePosition::new(8, 0, 8)], + ), + ])); +} + +#[test] +fn default_argument_when_not_provided() { + run_query( + r#"{ fieldWithDefaultArgumentValue }"#, + |result| { + assert_eq!( + result.get("fieldWithDefaultArgumentValue"), + Some(&Value::string(r#""Hello World""#))); + }); +} + +#[test] +fn default_argument_when_nullable_variable_not_provided() { + run_query( + r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#, + |result| { + assert_eq!( + result.get("fieldWithDefaultArgumentValue"), + Some(&Value::string(r#""Hello World""#))); + }); +} + +#[test] +fn default_argument_when_nullable_variable_set_to_null() { + run_variable_query( + r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#, + vec![ + ("input".to_owned(), InputValue::null()), + ].into_iter().collect(), + |result| { + assert_eq!( + result.get("fieldWithDefaultArgumentValue"), + Some(&Value::string(r#""Hello World""#))); + }); +} diff --git a/src/lib.rs b/src/lib.rs index 5d647e1c..8facda63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,7 @@ use std::collections::HashMap; use rustc_serialize::json::{ToJson, Json}; use parser::{parse_document_source, ParseError, Spanning, SourcePosition}; -use validation::{RuleError, ValidatorContext, visit_all_rules}; +use validation::{RuleError, ValidatorContext, visit_all_rules, validate_input_values}; use executor::execute_validated_query; pub use ast::{ToInputValue, FromInputValue, InputValue, Type, Selection}; @@ -242,6 +242,14 @@ pub fn execute<'a, CtxT, QueryT, MutationT>( { let document = try!(parse_document_source(document_source)); + { + let errors = validate_input_values(variables, &document, &root_node.schema); + + if !errors.is_empty() { + return Err(GraphQLError::ValidationError(errors)); + } + } + { let mut ctx = ValidatorContext::new(&root_node.schema, &document); visit_all_rules(&mut ctx, &document); diff --git a/src/macros/args.rs b/src/macros/args.rs index a5ffcefe..f6326fc1 100644 --- a/src/macros/args.rs +++ b/src/macros/args.rs @@ -21,17 +21,6 @@ macro_rules! __graphql__args { __graphql__args!(@assign_arg_vars, $args, $executorvar, $($rest)*); }; - ( - @assign_arg_vars, - $args:ident, $executorvar:ident, - $name:ident : Option<$ty:ty> as $desc:tt $($rest:tt)* - ) => { - let $name: Option<$ty> = $args - .get(&$crate::to_snake_case(stringify!($name))) - .unwrap_or(None); - __graphql__args!(@assign_arg_vars, $args, $executorvar, $($rest)*); - }; - ( @assign_arg_vars, $args:ident, $executorvar:ident, diff --git a/src/macros/input_object.rs b/src/macros/input_object.rs index b78aeb56..90b04077 100644 --- a/src/macros/input_object.rs +++ b/src/macros/input_object.rs @@ -50,8 +50,13 @@ macro_rules! graphql_input_object { Some($name { $( $field_name: { let n: String = $crate::to_snake_case(stringify!($field_name)); - let v: &$crate::InputValue = $var[&n[..]]; - $crate::FromInputValue::from(v).unwrap() + let v: Option<&&$crate::InputValue> = $var.get(&n[..]); + + if let Some(v) = v { + $crate::FromInputValue::from(v).unwrap() + } else { + $crate::FromInputValue::from(&$crate::InputValue::null()).unwrap() + } } ),* }) }; diff --git a/src/schema/model.rs b/src/schema/model.rs index cd37df2a..f1810a35 100644 --- a/src/schema/model.rs +++ b/src/schema/model.rs @@ -338,3 +338,13 @@ impl fmt::Display for DirectiveLocation { }) } } + +impl<'a> fmt::Display for TypeType<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TypeType::Concrete(ref t) => f.write_str(&t.name().unwrap()), + TypeType::List(ref i) => write!(f, "[{}]", i), + TypeType::NonNull(ref i) => write!(f, "{}!", i), + } + } +} diff --git a/src/types/base.rs b/src/types/base.rs index af40e608..11d0cf09 100644 --- a/src/types/base.rs +++ b/src/types/base.rs @@ -76,9 +76,11 @@ impl Arguments { if let (&mut Some(ref mut args), &Some(ref meta_args)) = (&mut args, meta_args) { for arg in meta_args { - if !args.contains_key(&arg.name) { + if !args.contains_key(&arg.name) || args[&arg.name].is_null() { if let Some(ref default_value) = arg.default_value { args.insert(arg.name.clone(), default_value.clone()); + } else { + args.insert(arg.name.clone(), InputValue::null()); } } } diff --git a/src/types/containers.rs b/src/types/containers.rs index 11501524..45c23efe 100644 --- a/src/types/containers.rs +++ b/src/types/containers.rs @@ -25,7 +25,7 @@ impl<T, CtxT> GraphQLType<CtxT> for Option<T> where T: GraphQLType<CtxT> { impl<T> FromInputValue for Option<T> where T: FromInputValue { fn from(v: &InputValue) -> Option<Option<T>> { match v { - &InputValue::Null => None, + &InputValue::Null => Some(None), v => match v.convert() { Some(x) => Some(Some(x)), None => None, @@ -79,7 +79,12 @@ impl<T> FromInputValue for Vec<T> where T: FromInputValue { None } }, - _ => None, + ref other => + if let Some(e) = other.convert() { + Some(vec![ e ]) + } else { + None + } } } } diff --git a/src/validation/input_value.rs b/src/validation/input_value.rs new file mode 100644 index 00000000..5413a9ab --- /dev/null +++ b/src/validation/input_value.rs @@ -0,0 +1,268 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; + +use parser::SourcePosition; +use ast::{InputValue, Document, Definition, VariableDefinitions}; +use validation::RuleError; +use schema::model::{SchemaType, TypeType}; +use schema::meta::{MetaType, ScalarMeta, InputObjectMeta, EnumMeta}; + +#[derive(Debug)] +enum Path<'a> { + Root, + ArrayElement(usize, &'a Path<'a>), + ObjectField(&'a str, &'a Path<'a>), +} + +pub fn validate_input_values( + values: &HashMap<String, InputValue>, + document: &Document, + schema: &SchemaType, +) + -> Vec<RuleError> +{ + let mut errs = vec![]; + + for def in document { + if let &Definition::Operation(ref op) = def { + if let Some(ref vars) = op.item.variable_definitions { + validate_var_defs(values, &vars.item, schema, &mut errs); + } + } + } + + errs.sort(); + errs +} + +fn validate_var_defs( + values: &HashMap<String, InputValue>, + var_defs: &VariableDefinitions, + schema: &SchemaType, + errors: &mut Vec<RuleError>, +) { + for &(ref name, ref def) in var_defs.iter() { + let raw_type_name = def.var_type.item.innermost_name(); + match schema.concrete_type_by_name(raw_type_name) { + Some(t) if t.is_input() => { + let ct = schema.make_type(&def.var_type.item); + + if def.var_type.item.is_non_null() && is_absent_or_null(values.get(&name.item)) { + errors.push(RuleError::new( + &format!( + r#"Variable "${}" of required type "{}" was not provided."#, + name.item, def.var_type.item, + ), + &[ name.start.clone() ], + )); + } else if let Some(ref v) = values.get(&name.item) { + unify_value(&name.item, &name.start, v, &ct, schema, errors, Path::Root); + } + }, + _ => errors.push(RuleError::new( + &format!( + r#"Variable "${}" expected value of type "{}" which cannot be used as an input type."#, + name.item, def.var_type.item, + ), + &[ name.start.clone() ], + )) + } + } +} + +fn unify_value<'a>( + var_name: &str, + var_pos: &SourcePosition, + value: &InputValue, + meta_type: &TypeType<'a>, + schema: &SchemaType, + errors: &mut Vec<RuleError>, + path: Path<'a>, +) { + match *meta_type { + TypeType::NonNull(ref inner) => { + if value.is_null() { + push_unification_error( + errors, var_name, var_pos, &path, + &format!(r#"Expected "{}", found null"#, meta_type) + ); + } + else { + unify_value(var_name, var_pos, value, &inner, schema, errors, path); + } + } + + TypeType::List(ref inner) => { + if value.is_null() { + return; + } + + match value.to_list_value() { + Some(l) => + for (i, v) in l.iter().enumerate() { + unify_value(var_name, var_pos, v, &inner, schema, errors, Path::ArrayElement(i, &path)); + }, + _ => unify_value(var_name, var_pos, value, &inner, schema, errors, path) + } + } + + TypeType::Concrete(mt) => { + if value.is_null() { + return; + } + + match mt { + &MetaType::Scalar(ref sm) => + unify_scalar(var_name, var_pos, value, sm, errors, &path), + &MetaType::Enum(ref em) => + unify_enum(var_name, var_pos, value, em, errors, &path), + &MetaType::InputObject(ref iom) => + unify_input_object(var_name, var_pos, value, iom, schema, errors, &path), + _ => panic!("Can't unify non-input concrete type"), + } + } + } +} + +fn unify_scalar<'a>( + var_name: &str, + var_pos: &SourcePosition, + value: &InputValue, + meta: &ScalarMeta, + errors: &mut Vec<RuleError>, + path: &Path<'a>, +) { + match value { + &InputValue::List(_) => + push_unification_error( + errors, + var_name, + var_pos, + path, + &format!(r#"Expected "{}", found list"#, meta.name), + ), + &InputValue::Object(_) => + push_unification_error( + errors, + var_name, + var_pos, + path, + &format!(r#"Expected "{}", found object"#, meta.name), + ), + _ => (), + } +} + +fn unify_enum<'a>( + var_name: &str, + var_pos: &SourcePosition, + value: &InputValue, + meta: &EnumMeta, + errors: &mut Vec<RuleError>, + path: &Path<'a>, +) { + match value { + &InputValue::String(_) | &InputValue::Enum(_) => (), + _ => push_unification_error( + errors, + var_name, + var_pos, + path, + &format!(r#"Expected "{}", found not a string or enum"#, meta.name), + ) + } +} + +fn unify_input_object<'a>( + var_name: &str, + var_pos: &SourcePosition, + value: &InputValue, + meta: &InputObjectMeta, + schema: &SchemaType, + errors: &mut Vec<RuleError>, + path: &Path<'a>, +) { + if let Some(ref obj) = value.to_object_value() { + let mut keys = obj.keys().collect::<HashSet<&&str>>(); + + for input_field in &meta.input_fields { + let mut has_value = false; + keys.remove(&input_field.name.as_str()); + + if let Some(ref value) = obj.get(input_field.name.as_str()) { + if !value.is_null() { + has_value = true; + + unify_value( + var_name, + var_pos, + value, + &schema.make_type(&input_field.arg_type), + schema, + errors, + Path::ObjectField(&input_field.name, path), + ); + } + } + + if !has_value && input_field.arg_type.is_non_null() { + push_unification_error( + errors, + var_name, + var_pos, + &Path::ObjectField(&input_field.name, path), + &format!(r#"Expected "{}", found null"#, input_field.arg_type), + ); + } + } + + for key in keys { + push_unification_error( + errors, + var_name, + var_pos, + &Path::ObjectField(&key, path), + &format!("Unknown field"), + ); + } + } + else { + push_unification_error( + errors, + var_name, + var_pos, + path, + &format!(r#"Expected "{}", found not an object"#, meta.name), + ); + } +} + +fn is_absent_or_null(v: Option<&InputValue>) -> bool { + v.map_or(true, InputValue::is_null) +} + +fn push_unification_error<'a>( + errors: &mut Vec<RuleError>, + var_name: &str, + var_pos: &SourcePosition, + path: &Path<'a>, + message: &str, +) { + errors.push(RuleError::new( + &format!( + r#"Variable "${}" got invalid value. {}{}."#, + var_name, path, message, + ), + &[ var_pos.clone() ], + )); +} + +impl<'a> fmt::Display for Path<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Path::Root => write!(f, ""), + Path::ArrayElement(idx, prev) => write!(f, "{}In element #{}: ", prev, idx), + Path::ObjectField(name, prev) => write!(f, r#"{}In field "{}": "#, prev, name), + } + } +} diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 7f7b58b3..ea2c98f3 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -5,6 +5,7 @@ mod traits; mod context; mod multi_visitor; mod rules; +mod input_value; #[cfg(test)] mod test_harness; @@ -14,6 +15,7 @@ pub use self::visitor::visit; pub use self::context::{RuleError, ValidatorContext}; pub use self::rules::visit_all_rules; pub use self::multi_visitor::MultiVisitor; +pub use self::input_value::validate_input_values; #[cfg(test)] pub use self::test_harness::{ diff --git a/src/validation/rules/variables_in_allowed_position.rs b/src/validation/rules/variables_in_allowed_position.rs index e21f3b62..007448c8 100644 --- a/src/validation/rules/variables_in_allowed_position.rs +++ b/src/validation/rules/variables_in_allowed_position.rs @@ -54,8 +54,6 @@ impl<'a> VariableInAllowedPosition<'a> { (_, t) => t.clone(), }; - println!("Variable {} of type {} used in position expecting {}", var_name.item, expected_type, var_type); - if !ctx.schema.is_subtype(&expected_type, var_type) { ctx.report_error( &error_message(&var_name.item, &format!("{}", expected_type), &format!("{}", var_type)),