Fix input value coercion with defaults (#1080, #1073)

This commit is contained in:
ilslv 2022-07-09 00:55:51 +03:00 committed by GitHub
parent 5332db0a4b
commit 0c8bcf582f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 366 additions and 72 deletions

View file

@ -48,7 +48,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Reworked [`chrono` crate] integration GraphQL scalars according to [graphql-scalars.dev] specs: ([#1010])
- Disabled `chrono` [Cargo feature] by default.
- Removed `scalar-naivetime` [Cargo feature].
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#528])
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
### Added
@ -72,8 +72,10 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Unsupported expressions in `graphql_value!` macro. ([#996], [#503])
- Incorrect GraphQL list coercion rules: `null` cannot be coerced to an `[Int!]!` or `[Int]!`. ([#1004])
- All procedural macros expansion inside `macro_rules!`. ([#1054], [#1051])
- Incorrect input value coercion with defaults. ([#1080], [#1073])
[#503]: /../../issues/503
[#528]: /../../issues/528
[#750]: /../../issues/750
[#798]: /../../issues/798
[#918]: /../../issues/918
@ -101,6 +103,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1054]: /../../pull/1054
[#1057]: /../../pull/1057
[#1060]: /../../pull/1060
[#1073]: /../../issues/1073
[#1080]: /../../pull/1080
[#1081]: /../../pull/1081
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083

View file

@ -295,25 +295,36 @@ impl<S> InputValue<S> {
Self::Object(o)
}
/// Resolve all variables to their values.
/// Resolves all variables of this [`InputValue`] to their actual `values`.
///
/// If a variable is not present in the `values`:
/// - Returns [`None`] in case this is an [`InputValue::Variable`].
/// - Skips field in case of an [`InputValue::Object`] field.
/// - Replaces with an [`InputValue::Null`] in case of an
/// [`InputValue::List`] element.
///
/// This is done, because for an [`InputValue::Variable`] (or an
/// [`InputValue::Object`] field) a default value can be used later, if it's
/// provided. While on contrary, a single [`InputValue::List`] element
/// cannot have a default value.
#[must_use]
pub fn into_const(self, vars: &Variables<S>) -> Self
pub fn into_const(self, values: &Variables<S>) -> Option<Self>
where
S: Clone,
{
match self {
Self::Variable(v) => vars.get(&v).map_or_else(InputValue::null, Clone::clone),
Self::List(l) => Self::List(
Self::Variable(v) => values.get(&v).cloned(),
Self::List(l) => Some(Self::List(
l.into_iter()
.map(|s| s.map(|v| v.into_const(vars)))
.map(|s| s.map(|v| v.into_const(values).unwrap_or_else(Self::null)))
.collect(),
),
Self::Object(o) => Self::Object(
)),
Self::Object(o) => Some(Self::Object(
o.into_iter()
.map(|(sk, sv)| (sk, sv.map(|v| v.into_const(vars))))
.filter_map(|(sk, sv)| sv.and_then(|v| v.into_const(values)).map(|sv| (sk, sv)))
.collect(),
),
v => v,
)),
v => Some(v),
}
}

View file

@ -1227,9 +1227,6 @@ impl<'r, S: 'r> Registry<'r, S> {
}
/// Creates an [`Argument`] with the provided default `value`.
///
/// When called with type `T`, the actual [`Argument`] will be given the
/// type `Option<T>`.
pub fn arg_with_default<T>(
&mut self,
name: &str,
@ -1240,7 +1237,7 @@ impl<'r, S: 'r> Registry<'r, S> {
T: GraphQLType<S> + ToInputValue<S> + FromInputValue<S>,
S: ScalarValue,
{
Argument::new(name, self.get_type::<Option<T>>(info)).default_value(value.to_input_value())
Argument::new(name, self.get_type::<T>(info)).default_value(value.to_input_value())
}
fn insert_placeholder(&mut self, name: Name, of_type: Type<'r>) {

View file

@ -462,6 +462,9 @@ async fn field_with_defaults_introspection() {
name
type {
name
ofType {
name
}
}
defaultValue
}
@ -477,12 +480,12 @@ async fn field_with_defaults_introspection() {
assert_eq!(fields.len(), 2);
assert!(fields.contains(&graphql_value!({
"name": "fieldOne",
"type": {"name": "Int"},
"type": {"name": null, "ofType": {"name": "Int"}},
"defaultValue": "123",
})));
assert!(fields.contains(&graphql_value!({
"name": "fieldTwo",
"type": {"name": "Int"},
"type": {"name": null, "ofType": {"name": "Int"}},
"defaultValue": "456",
})));
})

View file

@ -444,10 +444,14 @@ async fn object_introspection() {
"name": "second",
"description": "The second number",
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "Int",
"kind": "SCALAR",
"ofType": null,
},
},
"defaultValue": "123",
}],
"type": {

View file

@ -75,6 +75,12 @@ impl TestType {
format!("{:?}", input)
}
fn nullable_field_with_default_argument_value(
#[graphql(default = "Hello World".to_owned())] input: Option<String>,
) -> String {
format!("{:?}", input)
}
fn field_with_nested_object_input(input: Option<TestNestedInputObject>) -> String {
format!("{:?}", input)
}
@ -791,13 +797,14 @@ async fn default_argument_when_not_provided() {
}
#[tokio::test]
async fn default_argument_when_nullable_variable_not_provided() {
run_query(
r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#,
async fn provided_variable_overwrites_default_value() {
run_variable_query(
r#"query q($input: String!) { fieldWithDefaultArgumentValue(input: $input) }"#,
graphql_vars! {"input": "Overwritten"},
|result| {
assert_eq!(
result.get_field_value("fieldWithDefaultArgumentValue"),
Some(&graphql_value!(r#""Hello World""#)),
Some(&graphql_value!(r#""Overwritten""#)),
);
},
)
@ -805,14 +812,28 @@ async fn default_argument_when_nullable_variable_not_provided() {
}
#[tokio::test]
async fn default_argument_when_nullable_variable_set_to_null() {
async fn default_argument_when_nullable_variable_not_provided() {
run_query(
r#"query q($input: String) { nullableFieldWithDefaultArgumentValue(input: $input) }"#,
|result| {
assert_eq!(
result.get_field_value("nullableFieldWithDefaultArgumentValue"),
Some(&graphql_value!(r#"Some("Hello World")"#)),
);
},
)
.await;
}
#[tokio::test]
async fn null_when_nullable_variable_of_argument_with_default_value_set_to_null() {
run_variable_query(
r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#,
r#"query q($input: String) { nullableFieldWithDefaultArgumentValue(input: $input) }"#,
graphql_vars! {"input": null},
|result| {
assert_eq!(
result.get_field_value("fieldWithDefaultArgumentValue"),
Some(&graphql_value!(r#""Hello World""#)),
result.get_field_value("nullableFieldWithDefaultArgumentValue"),
Some(&graphql_value!(r#"None"#)),
);
},
)

View file

@ -81,7 +81,7 @@ impl<T> Spanning<T> {
}
}
/// Modify the contents of the spanned item
/// Modify the contents of the spanned item.
pub fn map<O, F: Fn(T) -> O>(self, f: F) -> Spanning<O> {
Spanning {
item: f(self.item),
@ -89,6 +89,13 @@ impl<T> Spanning<T> {
end: self.end,
}
}
/// Modifies the contents of the spanned item in case `f` returns [`Some`],
/// or returns [`None`] otherwise.
pub fn and_then<O, F: Fn(T) -> Option<O>>(self, f: F) -> Option<Spanning<O>> {
let (start, end) = (self.start, self.end);
f(self.item).map(|item| Spanning { item, start, end })
}
}
impl<T: fmt::Display> fmt::Display for Spanning<T> {

View file

@ -211,13 +211,19 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
}
}
fn fields(&self, #[graphql(default)] include_deprecated: bool) -> Option<Vec<&Field<S>>> {
fn fields(
&self,
#[graphql(default = false)] include_deprecated: Option<bool>,
) -> Option<Vec<&Field<S>>> {
match self {
TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. }))
| TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some(
fields
.iter()
.filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
.filter(|f| {
include_deprecated.unwrap_or_default()
|| !f.deprecation_status.is_deprecated()
})
.filter(|f| !f.name.starts_with("__"))
.collect(),
),
@ -302,12 +308,18 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
}
}
fn enum_values(&self, #[graphql(default)] include_deprecated: bool) -> Option<Vec<&EnumValue>> {
fn enum_values(
&self,
#[graphql(default = false)] include_deprecated: Option<bool>,
) -> Option<Vec<&EnumValue>> {
match self {
TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some(
values
.iter()
.filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
.filter(|f| {
include_deprecated.unwrap_or_default()
|| !f.deprecation_status.is_deprecated()
})
.collect(),
),
_ => None,

View file

@ -242,7 +242,9 @@ where
f.arguments.as_ref().map(|m| {
m.item
.iter()
.map(|&(ref k, ref v)| (k.item, v.item.clone().into_const(exec_vars)))
.filter_map(|&(ref k, ref v)| {
v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
})
.collect()
}),
&meta_field.arguments,

View file

@ -49,7 +49,6 @@ pub enum TypeKind {
/// ## Input objects
///
/// Represents complex values provided in queries _into_ the system.
#[graphql(name = "INPUT_OBJECT")]
InputObject,
/// ## List types
@ -63,7 +62,6 @@ pub enum TypeKind {
///
/// In GraphQL, nullable types are the default. By putting a `!` after a\
/// type, it becomes non-nullable.
#[graphql(name = "NON_NULL")]
NonNull,
}
@ -89,7 +87,7 @@ impl<'a, S> Arguments<'a, S> {
if let (Some(args), Some(meta_args)) = (&mut args, meta_args) {
for arg in meta_args {
let arg_name = arg.name.as_str();
if args.get(arg_name).map_or(true, InputValue::is_null) {
if args.get(arg_name).is_none() {
if let Some(val) = arg.default_value.as_ref() {
args.insert(arg_name, val.clone());
}
@ -474,8 +472,8 @@ where
f.arguments.as_ref().map(|m| {
m.item
.iter()
.map(|&(ref k, ref v)| {
(k.item, v.item.clone().into_const(exec_vars))
.filter_map(|&(ref k, ref v)| {
v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
})
.collect()
}),
@ -608,7 +606,7 @@ where
.arguments
.iter()
.flat_map(|m| m.item.get("if"))
.flat_map(|v| v.item.clone().into_const(vars).convert())
.filter_map(|v| v.item.clone().into_const(vars)?.convert().ok())
.next()
.unwrap();

View file

@ -316,7 +316,9 @@ where
f.arguments.as_ref().map(|m| {
m.item
.iter()
.map(|&(ref k, ref v)| (k.item, v.item.clone().into_const(exec_vars)))
.filter_map(|&(ref k, ref v)| {
v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
})
.collect()
}),
&meta_field.arguments,

View file

@ -71,7 +71,7 @@ where
let mut remaining_required_fields = input_fields
.iter()
.filter_map(|f| {
if f.arg_type.is_non_null() {
if f.arg_type.is_non_null() && f.default_value.is_none() {
Some(&f.name)
} else {
None

View file

@ -26,6 +26,7 @@ where
{
for meta_arg in meta_args {
if meta_arg.arg_type.is_non_null()
&& meta_arg.default_value.is_none()
&& field
.item
.arguments

View file

@ -57,9 +57,11 @@ impl ToTokens for Value {
match self {
Self::Default => quote! {
::std::default::Default::default()
},
Self::Expr(expr) => quote! {
(#expr).into()
},
}
.to_tokens(into),
Self::Expr(expr) => expr.to_tokens(into),
}
.to_tokens(into)
}
}

View file

@ -1,6 +1,9 @@
//! Tests for `#[derive(GraphQLInputObject)]` macro.
use juniper::{execute, graphql_object, graphql_value, graphql_vars, GraphQLInputObject};
use juniper::{
execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition, GraphQLError,
GraphQLInputObject, RuleError,
};
use crate::util::schema;
@ -148,16 +151,56 @@ mod default_value {
#[tokio::test]
async fn resolves() {
const DOC: &str = r#"{
x(point: { y: 20 })
x2: x(point: { x: 20 })
const DOC: &str = r#"query q($ve_num: Float!) {
literal_implicit_other_number: x(point: { y: 20 })
literal_explicit_number: x(point: { x: 20 })
literal_implicit_all: x(point: {})
variable_explicit_number: x(point: { x: $ve_num })
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {"ve_num": 40}, &()).await,
Ok((
graphql_value!({
"literal_implicit_other_number": 10.0,
"literal_explicit_number": 20.0,
"literal_implicit_all": 10.0,
"variable_explicit_number": 40.0,
}),
vec![],
)),
);
}
#[tokio::test]
async fn errs_on_explicit_null_literal() {
const DOC: &str = r#"{ x(point: { x: 20, y: null }) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"x": 10.0, "x2": 20.0}), vec![])),
Err(GraphQLError::ValidationError(vec![RuleError::new(
"Invalid value for argument \"point\", expected type \"Point2D!\"",
&[SourcePosition::new(11, 0, 11)],
)]))
);
}
#[tokio::test]
async fn errs_on_missing_variable() {
const DOC: &str = r#"query q($x: Float!){ x(point: { x: $x }) }"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Err(GraphQLError::ValidationError(vec![RuleError::new(
"Variable \"$x\" of required type \"Float!\" was not provided.",
&[SourcePosition::new(8, 0, 8)],
)]))
);
}
@ -196,6 +239,140 @@ mod default_value {
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
graphql_value!({"__type": {"inputFields": [{
"name": "x",
"description": null,
"type": {"ofType": {"name": "Float"}},
"defaultValue": "10",
}, {
"name": "y",
"description": null,
"type": {"ofType": {"name": "Float"}},
"defaultValue": "10",
}]}}),
vec![],
)),
);
}
}
mod default_nullable_value {
use super::*;
#[derive(GraphQLInputObject)]
struct Point2D {
#[graphql(default = 10.0)]
x: Option<f64>,
#[graphql(default = 10.0)]
y: Option<f64>,
}
struct QueryRoot;
#[graphql_object]
impl QueryRoot {
fn x(point: Point2D) -> Option<f64> {
point.x
}
}
#[tokio::test]
async fn resolves() {
const DOC: &str = r#"query q(
$ve_num: Float,
$ve_null: Float,
$vi: Float,
$vde_num: Float = 40,
$vde_null: Float = 50,
$vdi: Float = 60,
) {
literal_implicit_other_number: x(point: { y: 20 })
literal_explicit_number: x(point: { x: 20 })
literal_implicit_all: x(point: {})
literal_explicit_null: x(point: { x: null })
literal_implicit_other_null: x(point: { y: null })
variable_explicit_number: x(point: { x: $ve_num })
variable_explicit_null: x(point: { x: $ve_null })
variable_implicit: x(point: { x: $vi })
variable_default_explicit_number: x(point: { x: $vde_num })
variable_default_explicit_null: x(point: { x: $vde_null })
variable_default_implicit: x(point: { x: $vdi })
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(
DOC,
None,
&schema,
&graphql_vars! {
"ve_num": 30.0,
"ve_null": null,
"vde_num": 100,
"vde_null": null,
},
&(),
)
.await,
Ok((
graphql_value!({
"literal_implicit_other_number": 10.0,
"literal_explicit_number": 20.0,
"literal_implicit_all": 10.0,
"literal_explicit_null": null,
"literal_implicit_other_null": 10.0,
"variable_explicit_number": 30.0,
"variable_explicit_null": null,
"variable_implicit": 10.0,
"variable_default_explicit_number": 100.0,
"variable_default_explicit_null": null,
"variable_default_implicit": 60.0,
}),
vec![],
)),
);
}
#[tokio::test]
async fn is_graphql_input_object() {
const DOC: &str = r#"{
__type(name: "Point2D") {
kind
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])),
);
}
#[tokio::test]
async fn has_input_fields() {
const DOC: &str = r#"{
__type(name: "Point2D") {
inputFields {
name
description
type {
name
ofType {
name
}
}
defaultValue
}
}
}"#;
let schema = schema(QueryRoot);
assert_eq!(
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
Ok((
@ -203,13 +380,13 @@ mod default_value {
{
"name": "x",
"description": null,
"type": {"ofType": null},
"type": {"name": "Float", "ofType": null},
"defaultValue": "10",
},
{
"name": "y",
"description": null,
"type": {"ofType": null},
"type": {"name": "Float", "ofType": null},
"defaultValue": "10",
},
]}}),

View file

@ -1311,21 +1311,21 @@ mod default_argument {
"args": [{
"name": "first",
"defaultValue": r#""""#,
"type": {"name": "String", "ofType": null},
"type": {"name": null, "ofType": {"name": "String"}},
}, {
"name": "second",
"defaultValue": r#""second""#,
"type": {"name": "String", "ofType": null},
"type": {"name": null, "ofType": {"name": "String"}},
}, {
"name": "third",
"defaultValue": r#""t""#,
"type": {"name": "String", "ofType": null},
"type": {"name": null, "ofType": {"name": "String"}},
}],
}, {
"args": [{
"name": "coord",
"defaultValue": "{x: 1}",
"type": {"name": "Point", "ofType": null},
"type": {"name": null, "ofType": {"name": "Point"}},
}],
}]}}),
vec![],

View file

@ -1045,10 +1045,10 @@ mod default_argument {
impl Human {
fn id(
#[graphql(default)] arg1: i32,
#[graphql(default = "second".to_string())] arg2: String,
#[graphql(default = "second".to_string())] arg2: Option<String>,
#[graphql(default = true)] r#arg3: bool,
) -> String {
format!("{}|{}&{}", arg1, arg2, r#arg3)
format!("{}|{:?}&{}", arg1, arg2, r#arg3)
}
fn info(#[graphql(default = Point { x: 1 })] coord: Point) -> i32 {
@ -1069,20 +1069,72 @@ mod default_argument {
async fn resolves_id_field() {
let schema = schema(QueryRoot);
for (input, expected) in &[
("{ human { id } }", "0|second&true"),
("{ human { id(arg1: 1) } }", "1|second&true"),
(r#"{ human { id(arg2: "") } }"#, "0|&true"),
(r#"{ human { id(arg1: 2, arg2: "") } }"#, "2|&true"),
for (input, expected, vars) in &[
(
"{ human { id } }",
r#"0|Some("second")&true"#,
graphql_vars! {},
),
(
"{ human { id(arg1: 1) } }",
r#"1|Some("second")&true"#,
graphql_vars! {},
),
(
r#"{ human { id(arg2: "other") } }"#,
r#"0|Some("other")&true"#,
graphql_vars! {},
),
(
"{ human { id(arg2: null) } }",
r#"0|None&true"#,
graphql_vars! {},
),
(
"query q($arg2: String) { human { id(arg2: $arg2) } }",
r#"0|Some("second")&true"#,
graphql_vars! {},
),
(
"query q($arg2: String) { human{ id(arg2: $arg2) } }",
r#"0|None&true"#,
graphql_vars! { "arg2": null },
),
(
"query q($arg2: String) { human{ id(arg2: $arg2) } }",
r#"0|Some("other")&true"#,
graphql_vars! { "arg2": "other" },
),
(
r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
r#"0|Some("other")&true"#,
graphql_vars! {},
),
(
r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
r#"0|None&true"#,
graphql_vars! { "arg2": null },
),
(
r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
r#"0|Some("hello")&true"#,
graphql_vars! { "arg2": "hello" },
),
(
r#"{ human { id(arg1: 2, arg2: "") } }"#,
r#"2|Some("")&true"#,
graphql_vars! {},
),
(
r#"{ human { id(arg1: 1, arg2: "", arg3: false) } }"#,
"1|&false",
r#"1|Some("")&false"#,
graphql_vars! {},
),
] {
let expected: &str = *expected;
assert_eq!(
execute(*input, None, &schema, &graphql_vars! {}, &()).await,
execute(*input, None, &schema, &vars, &(),).await,
Ok((graphql_value!({"human": {"id": expected}}), vec![])),
);
}
@ -1133,7 +1185,7 @@ mod default_argument {
"args": [{
"name": "arg1",
"defaultValue": "0",
"type": {"name": "Int", "ofType": null},
"type": {"name": null, "ofType": {"name": "Int"}},
}, {
"name": "arg2",
"defaultValue": r#""second""#,
@ -1141,13 +1193,13 @@ mod default_argument {
}, {
"name": "arg3",
"defaultValue": "true",
"type": {"name": "Boolean", "ofType": null},
"type": {"name": null, "ofType": {"name": "Boolean"}},
}],
}, {
"args": [{
"name": "coord",
"defaultValue": "{x: 1}",
"type": {"name": "Point", "ofType": null},
"type": {"name": null, "ofType": {"name": "Point"}},
}],
}]}}),
vec![],

View file

@ -607,21 +607,21 @@ mod default_argument {
"args": [{
"name": "arg1",
"defaultValue": "0",
"type": {"name": "Int", "ofType": null},
"type": {"name": null, "ofType": {"name": "Int"}},
}, {
"name": "arg2",
"defaultValue": r#""second""#,
"type": {"name": "String", "ofType": null},
"type": {"name": null, "ofType": {"name": "String"}},
}, {
"name": "arg3",
"defaultValue": "true",
"type": {"name": "Boolean", "ofType": null},
"type": {"name": null, "ofType": {"name": "Boolean"}},
}],
}, {
"args": [{
"name": "coord",
"defaultValue": "{x: 1}",
"type": {"name": "Point", "ofType": null},
"type": {"name": null, "ofType": {"name": "Point"}},
}],
}]}}),
vec![],