Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add descriptive value validation errors #811

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion juniper/src/executor_tests/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ async fn does_not_accept_string_literals() {
assert_eq!(
error,
ValidationError(vec![RuleError::new(
r#"Invalid value for argument "color", expected type "Color!""#,
r#"Invalid value for argument "color", reason: Invalid value ""RED"" for enum "Color""#,
&[SourcePosition::new(18, 0, 18)],
)])
);
Expand Down
7 changes: 5 additions & 2 deletions juniper/src/executor_tests/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,8 @@ async fn does_not_allow_missing_required_field() {
assert_eq!(
error,
ValidationError(vec![RuleError::new(
r#"Invalid value for argument "arg", expected type "ExampleInputObject!""#,
"Invalid value for argument \"arg\", \
reason: \"ExampleInputObject\" is missing fields: \"b\"",
&[SourcePosition::new(20, 0, 20)],
)]),
);
Expand All @@ -940,7 +941,9 @@ async fn does_not_allow_null_in_required_field() {
assert_eq!(
error,
ValidationError(vec![RuleError::new(
r#"Invalid value for argument "arg", expected type "ExampleInputObject!""#,
"Invalid value for argument \"arg\", \
reason: Error on \"ExampleInputObject\" field \"b\": \
\"null\" specified for not nullable type \"Int!\"",
&[SourcePosition::new(20, 0, 20)],
)]),
);
Expand Down
150 changes: 121 additions & 29 deletions juniper/src/types/utilities.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,126 @@
use std::{collections::HashSet, iter::Iterator};

use crate::{
ast::InputValue,
schema::{
meta::{EnumMeta, InputObjectMeta, MetaType},
meta::{Argument, EnumMeta, InputObjectMeta, MetaType},
model::{SchemaType, TypeType},
},
value::ScalarValue,
};
use std::collections::HashSet;

pub fn is_valid_literal_value<S>(
/// Common error messages used in validation and execution of GraphQL operations
pub(crate) mod error {
use std::fmt::Display;

pub(crate) fn non_null(arg_type: impl Display) -> String {
format!("\"null\" specified for not nullable type \"{arg_type}\"")
}

pub(crate) fn enum_value(arg_value: impl Display, arg_type: impl Display) -> String {
format!("Invalid value \"{arg_value}\" for enum \"{arg_type}\"")
}

pub(crate) fn type_value(arg_value: impl Display, arg_type: impl Display) -> String {
format!("Invalid value \"{arg_value}\" for type \"{arg_type}\"")
}

pub(crate) fn parser(arg_type: impl Display, msg: impl Display) -> String {
format!("Parser error for \"{arg_type}\": {msg}")
}

pub(crate) fn not_input_object(arg_type: impl Display) -> String {
format!("\"{arg_type}\" is not an input object")
}

pub(crate) fn field(
arg_type: impl Display,
field_name: impl Display,
error_message: impl Display,
) -> String {
format!("Error on \"{arg_type}\" field \"{field_name}\": {error_message}")
}

pub(crate) fn missing_fields(arg_type: impl Display, missing_fields: impl Display) -> String {
format!("\"{arg_type}\" is missing fields: {missing_fields}")
}

pub(crate) fn unknown_field(arg_type: impl Display, field_name: impl Display) -> String {
format!("Field \"{field_name}\" does not exist on type \"{arg_type}\"")
}

pub(crate) fn invalid_list_length(
arg_value: impl Display,
actual: usize,
expected: usize,
) -> String {
format!("Expected list of length {actual}, but \"{arg_value}\" has length \"{expected}\"")
}
}

/// Validates the specified field of a GraphQL object and returns an error message if the field is
/// invalid.
fn validate_object_field<S>(
schema: &SchemaType<S>,
object_type: &TypeType<S>,
object_fields: &[Argument<S>],
field_value: &InputValue<S>,
field_key: &str,
) -> Option<String>
where
S: ScalarValue,
{
let field_type = object_fields
.iter()
.filter(|f| f.name == field_key)
.map(|f| schema.make_type(&f.arg_type))
.next();

if let Some(field_arg_type) = field_type {
let error_message = validate_literal_value(schema, &field_arg_type, field_value);

error_message.map(|m| error::field(object_type, field_key, m))
} else {
Some(error::unknown_field(object_type, field_key))
}
}

/// Validates the specified GraphQL literal and returns an error message if the it's invalid.
pub fn validate_literal_value<S>(
schema: &SchemaType<S>,
arg_type: &TypeType<S>,
arg_value: &InputValue<S>,
) -> bool
) -> Option<String>
where
S: ScalarValue,
{
match *arg_type {
TypeType::NonNull(ref inner) => {
if arg_value.is_null() {
false
Some(error::non_null(arg_type))
} else {
is_valid_literal_value(schema, inner, arg_value)
validate_literal_value(schema, inner, arg_value)
}
}
TypeType::List(ref inner, expected_size) => match *arg_value {
InputValue::Null | InputValue::Variable(_) => true,
InputValue::Null | InputValue::Variable(_) => None,
InputValue::List(ref items) => {
if let Some(expected) = expected_size {
if items.len() != expected {
return false;
return Some(error::invalid_list_length(arg_value, items.len(), expected));
}
}
items
.iter()
.all(|i| is_valid_literal_value(schema, inner, &i.item))
.find_map(|i| validate_literal_value(schema, inner, &i.item))
}
ref v => {
if let Some(expected) = expected_size {
if expected != 1 {
return false;
return Some(error::invalid_list_length(arg_value, 1, expected));
}
}
is_valid_literal_value(schema, inner, v)
validate_literal_value(schema, inner, v)
}
},
TypeType::Concrete(t) => {
Expand All @@ -51,19 +129,23 @@ where
if let (&InputValue::Scalar(_), Some(&MetaType::Enum(EnumMeta { .. }))) =
(arg_value, arg_type.to_concrete())
{
return false;
return Some(error::enum_value(arg_value, arg_type));
}

match *arg_value {
InputValue::Null | InputValue::Variable(_) => true,
InputValue::Null | InputValue::Variable(_) => None,
ref v @ InputValue::Scalar(_) | ref v @ InputValue::Enum(_) => {
if let Some(parse_fn) = t.input_value_parse_fn() {
parse_fn(v).is_ok()
if parse_fn(v).is_ok() {
None
} else {
Some(error::type_value(arg_value, arg_type))
}
} else {
false
Some(error::parser(arg_type, "no parser present"))
}
}
InputValue::List(_) => false,
InputValue::List(_) => Some("Input lists are not literals".to_owned()),
InputValue::Object(ref obj) => {
if let MetaType::InputObject(InputObjectMeta {
ref input_fields, ..
Expand All @@ -77,23 +159,33 @@ where
})
.collect::<HashSet<_>>();

let all_types_ok = obj.iter().all(|(key, value)| {
let error_message = obj.iter().find_map(|(key, value)| {
remaining_required_fields.remove(&key.item);
if let Some(ref arg_type) = input_fields
.iter()
.filter(|f| f.name == key.item)
.map(|f| schema.make_type(&f.arg_type))
.next()
{
is_valid_literal_value(schema, arg_type, &value.item)
} else {
false
}
validate_object_field(
schema,
arg_type,
input_fields,
&value.item,
&key.item,
)
});

all_types_ok && remaining_required_fields.is_empty()
if error_message.is_some() {
return error_message;
}

if remaining_required_fields.is_empty() {
None
} else {
let missing_fields = remaining_required_fields
.into_iter()
.map(|s| format!("\"{}\"", &**s))
.collect::<Vec<_>>()
.join(", ");
Some(error::missing_fields(arg_type, missing_fields))
}
} else {
false
Some(error::not_input_object(arg_type))
}
}
}
Expand Down
Loading
Loading