Add input variable tests, fix a bunch of related bugs
This commit is contained in:
parent
ba7839f321
commit
b3a750ffa5
12 changed files with 1046 additions and 20 deletions
|
@ -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()
|
||||
),
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
mod introspection;
|
||||
mod variables;
|
||||
|
|
737
src/executor_tests/variables.rs
Normal file
737
src/executor_tests/variables.rs
Normal file
|
@ -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""#)));
|
||||
});
|
||||
}
|
10
src/lib.rs
10
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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
} ),*
|
||||
})
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
268
src/validation/input_value.rs
Normal file
268
src/validation/input_value.rs
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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::{
|
||||
|
|
|
@ -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)),
|
||||
|
|
Loading…
Reference in a new issue