diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 88e58f9f..c90fc5bc 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -24,6 +24,7 @@ - Allow spreading interface fragments on unions and other interfaces. ([#965](https://github.com/graphql-rust/juniper/pull/965), [#798](https://github.com/graphql-rust/juniper/issues/798)) - Support expressions in `graphql_value!` macro. ([#996](https://github.com/graphql-rust/juniper/pull/996), [#503](https://github.com/graphql-rust/juniper/issues/503)) +- List coercion rules: `null` cannot be coerced to an `[Int!]!` or `[Int]!`. ([#1004](https://github.com/graphql-rust/juniper/pull/1004)) # [[0.15.7] 2021-07-08](https://github.com/graphql-rust/juniper/releases/tag/juniper-v0.15.7) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 0018acdd..c8812221 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -49,10 +49,13 @@ serde_json = { version = "1.0.2", default-features = false, optional = true } smartstring = "0.2.6" static_assertions = "1.1" url = { version = "2.0", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "0.8", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "0.8", default-features = false, features = ["wasm-bindgen"], optional = true } [dev-dependencies] bencher = "0.1.2" diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs index 580a8c98..55da456b 100644 --- a/juniper/src/types/containers.rs +++ b/juniper/src/types/containers.rs @@ -157,17 +157,22 @@ where } } -impl> FromInputValue for Vec { - type Error = T::Error; +impl> FromInputValue for Vec { + type Error = FromInputValueVecError; fn from_input_value(v: &InputValue) -> Result { match v { - InputValue::List(l) => l.iter().map(|i| i.item.convert()).collect(), + InputValue::List(l) => l + .iter() + .map(|i| i.item.convert().map_err(FromInputValueVecError::Item)) + .collect(), // See "Input Coercion" on List types: - // https://spec.graphql.org/June2018/#sec-Type-System.List - // In reality is intercepted by `Option`. - InputValue::Null => Ok(Vec::new()), - other => other.convert().map(|e| vec![e]), + // https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + InputValue::Null => Err(FromInputValueVecError::Null), + other => other + .convert() + .map(|e| vec![e]) + .map_err(FromInputValueVecError::Item), } } } @@ -182,6 +187,39 @@ where } } +/// Possible errors of converting [`InputValue`] into [`Vec`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum FromInputValueVecError +where + T: FromInputValue, + S: ScalarValue, +{ + /// [`InputValue`] cannot be [`Null`]. + /// + /// See ["Combining List and Non-Null" section of spec][1]. + /// + /// [`Null`]: [`InputValue::Null`] + /// [1]: https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + Null, + + /// Error of converting [`InputValue::List`]'s item. + Item(T::Error), +} + +impl IntoFieldError for FromInputValueVecError +where + T: FromInputValue, + T::Error: IntoFieldError, + S: ScalarValue, +{ + fn into_field_error(self) -> FieldError { + match self { + Self::Null => "Failed to convert into `Vec`: Value cannot be `null`".into(), + Self::Item(s) => s.into_field_error(), + } + } +} + impl GraphQLType for [T] where S: ScalarValue, @@ -379,7 +417,7 @@ where if let Some(i) = items .next() .transpose() - .map_err(FromInputValueArrayError::Scalar)? + .map_err(FromInputValueArrayError::Item)? { *elem = MaybeUninit::new(i); out.init_len += 1; @@ -402,21 +440,12 @@ where Ok(unsafe { mem::transmute_copy::<_, Self>(&out.arr) }) } // See "Input Coercion" on List types: - // https://spec.graphql.org/June2018/#sec-Type-System.List - // In reality is intercepted by `Option`. - InputValue::Null if N == 0 => { - // TODO: Use `mem::transmute` instead of - // `mem::transmute_copy` below, once it's allowed - // for const generics: - // https://github.com/rust-lang/rust/issues/61956 - // SAFETY: `mem::transmute_copy` is safe here, because we check - // `N` to be `0`. It's no-op, actually. - Ok(unsafe { mem::transmute_copy::<[T; 0], Self>(&[]) }) - } + // https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + InputValue::Null => Err(FromInputValueArrayError::Null), ref other => { other .convert() - .map_err(FromInputValueArrayError::Scalar) + .map_err(FromInputValueArrayError::Item) .and_then(|e: T| { // TODO: Use `mem::transmute` instead of // `mem::transmute_copy` below, once it's allowed @@ -457,22 +486,31 @@ where } /// Error converting [`InputValue`] into exact-size [`array`](prim@array). +#[derive(Clone, Debug, Eq, PartialEq)] pub enum FromInputValueArrayError where T: FromInputValue, S: ScalarValue, { - /// Wrong count of elements. + /// [`InputValue`] cannot be [`Null`]. + /// + /// See ["Combining List and Non-Null" section of spec][1]. + /// + /// [`Null`]: [`InputValue::Null`] + /// [1]: https://spec.graphql.org/October2021#sec-Combining-List-and-Non-Null + Null, + + /// Wrong count of items. WrongCount { - /// Actual count of elements. + /// Actual count of items. actual: usize, - /// Expected count of elements. + /// Expected count of items. expected: usize, }, - /// Underlying [`ScalarValue`] conversion error. - Scalar(T::Error), + /// Error of converting [`InputValue::List`]'s item. + Item(T::Error), } impl IntoFieldError for FromInputValueArrayError @@ -484,12 +522,13 @@ where fn into_field_error(self) -> FieldError { const ERROR_PREFIX: &str = "Failed to convert into exact-size array"; match self { + Self::Null => format!("{}: Value cannot be `null`", ERROR_PREFIX).into(), Self::WrongCount { actual, expected } => format!( "{}: wrong elements count: {} instead of {}", ERROR_PREFIX, actual, expected ) .into(), - Self::Scalar(s) => s.into_field_error(), + Self::Item(s) => s.into_field_error(), } } } @@ -562,14 +601,228 @@ where mod coercion { use crate::{graphql_input_value, FromInputValue as _, InputValue}; + use super::{FromInputValueArrayError, FromInputValueVecError}; + type V = InputValue; + #[test] + fn option() { + let v: V = graphql_input_value!(null); + assert_eq!(>::from_input_value(&v), Ok(None)); + + let v: V = graphql_input_value!(1); + assert_eq!(>::from_input_value(&v), Ok(Some(1))); + } + // See "Input Coercion" examples on List types: - // https://spec.graphql.org/June2018/#sec-Type-System.List + // https://spec.graphql.org/October2021/#sec-List.Input-Coercion #[test] fn vec() { + let v: V = graphql_input_value!(null); + assert_eq!( + >::from_input_value(&v), + Err(FromInputValueVecError::Null), + ); + assert_eq!( + >>::from_input_value(&v), + Err(FromInputValueVecError::Null), + ); + assert_eq!(>>::from_input_value(&v), Ok(None)); + assert_eq!(>>>::from_input_value(&v), Ok(None)); + assert_eq!( + >>::from_input_value(&v), + Err(FromInputValueVecError::Null), + ); + assert_eq!( + >>>>>::from_input_value(&v), + Ok(None), + ); + + let v: V = graphql_input_value!(1); + assert_eq!(>::from_input_value(&v), Ok(vec![1])); + assert_eq!(>>::from_input_value(&v), Ok(vec![Some(1)])); + assert_eq!(>>::from_input_value(&v), Ok(Some(vec![1]))); + assert_eq!( + >>>::from_input_value(&v), + Ok(Some(vec![Some(1)])), + ); + assert_eq!(>>::from_input_value(&v), Ok(vec![vec![1]])); + assert_eq!( + >>>>>::from_input_value(&v), + Ok(Some(vec![Some(vec![Some(1)])])), + ); + let v: V = graphql_input_value!([1, 2, 3]); - assert_eq!(>::from_input_value(&v), Ok(vec![1, 2, 3]),); - // TODO: all examples + assert_eq!(>::from_input_value(&v), Ok(vec![1, 2, 3])); + assert_eq!( + >>::from_input_value(&v), + Ok(Some(vec![1, 2, 3])), + ); + assert_eq!( + >>::from_input_value(&v), + Ok(vec![Some(1), Some(2), Some(3)]), + ); + assert_eq!( + >>>::from_input_value(&v), + Ok(Some(vec![Some(1), Some(2), Some(3)])), + ); + assert_eq!( + >>::from_input_value(&v), + Ok(vec![vec![1], vec![2], vec![3]]), + ); + // Looks like the spec ambiguity. + // See: https://github.com/graphql/graphql-spec/pull/515 + assert_eq!( + >>>>>::from_input_value(&v), + Ok(Some(vec![ + Some(vec![Some(1)]), + Some(vec![Some(2)]), + Some(vec![Some(3)]), + ])), + ); + + let v: V = graphql_input_value!([1, 2, null]); + assert_eq!( + >::from_input_value(&v), + Err(FromInputValueVecError::Item( + "Expected `Int`, found: null".to_owned(), + )), + ); + assert_eq!( + >>::from_input_value(&v), + Err(FromInputValueVecError::Item( + "Expected `Int`, found: null".to_owned(), + )), + ); + assert_eq!( + >>::from_input_value(&v), + Ok(vec![Some(1), Some(2), None]), + ); + assert_eq!( + >>>::from_input_value(&v), + Ok(Some(vec![Some(1), Some(2), None])), + ); + assert_eq!( + >>::from_input_value(&v), + Err(FromInputValueVecError::Item(FromInputValueVecError::Null)), + ); + // Looks like the spec ambiguity. + // See: https://github.com/graphql/graphql-spec/pull/515 + assert_eq!( + >>>>>::from_input_value(&v), + Ok(Some(vec![Some(vec![Some(1)]), Some(vec![Some(2)]), None])), + ); + } + + // See "Input Coercion" examples on List types: + // https://spec.graphql.org/October2021#sec-List.Input-Coercion + #[test] + fn array() { + let v: V = graphql_input_value!(null); + assert_eq!( + <[i32; 0]>::from_input_value(&v), + Err(FromInputValueArrayError::Null), + ); + assert_eq!( + <[i32; 1]>::from_input_value(&v), + Err(FromInputValueArrayError::Null), + ); + assert_eq!( + <[Option; 0]>::from_input_value(&v), + Err(FromInputValueArrayError::Null), + ); + assert_eq!( + <[Option; 1]>::from_input_value(&v), + Err(FromInputValueArrayError::Null), + ); + assert_eq!(>::from_input_value(&v), Ok(None)); + assert_eq!(>::from_input_value(&v), Ok(None)); + assert_eq!(; 0]>>::from_input_value(&v), Ok(None)); + assert_eq!(; 1]>>::from_input_value(&v), Ok(None)); + assert_eq!( + <[[i32; 1]; 1]>::from_input_value(&v), + Err(FromInputValueArrayError::Null), + ); + assert_eq!( + ; 1]>; 1]>>::from_input_value(&v), + Ok(None), + ); + + let v: V = graphql_input_value!(1); + assert_eq!(<[i32; 1]>::from_input_value(&v), Ok([1])); + assert_eq!( + <[i32; 0]>::from_input_value(&v), + Err(FromInputValueArrayError::WrongCount { + expected: 0, + actual: 1, + }), + ); + assert_eq!(<[Option; 1]>::from_input_value(&v), Ok([Some(1)])); + assert_eq!(>::from_input_value(&v), Ok(Some([1]))); + assert_eq!( + ; 1]>>::from_input_value(&v), + Ok(Some([Some(1)])), + ); + assert_eq!(<[[i32; 1]; 1]>::from_input_value(&v), Ok([[1]])); + assert_eq!( + ; 1]>; 1]>>::from_input_value(&v), + Ok(Some([Some([Some(1)])])), + ); + + let v: V = graphql_input_value!([1, 2, 3]); + assert_eq!(<[i32; 3]>::from_input_value(&v), Ok([1, 2, 3])); + assert_eq!( + >::from_input_value(&v), + Ok(Some([1, 2, 3])), + ); + assert_eq!( + <[Option; 3]>::from_input_value(&v), + Ok([Some(1), Some(2), Some(3)]), + ); + assert_eq!( + ; 3]>>::from_input_value(&v), + Ok(Some([Some(1), Some(2), Some(3)])), + ); + assert_eq!(<[[i32; 1]; 3]>::from_input_value(&v), Ok([[1], [2], [3]])); + // Looks like the spec ambiguity. + // See: https://github.com/graphql/graphql-spec/pull/515 + assert_eq!( + ; 1]>; 3]>>::from_input_value(&v), + Ok(Some([Some([Some(1)]), Some([Some(2)]), Some([Some(3)]),])), + ); + + let v: V = graphql_input_value!([1, 2, null]); + assert_eq!( + <[i32; 3]>::from_input_value(&v), + Err(FromInputValueArrayError::Item( + "Expected `Int`, found: null".to_owned(), + )), + ); + assert_eq!( + >::from_input_value(&v), + Err(FromInputValueArrayError::Item( + "Expected `Int`, found: null".to_owned(), + )), + ); + assert_eq!( + <[Option; 3]>::from_input_value(&v), + Ok([Some(1), Some(2), None]), + ); + assert_eq!( + ; 3]>>::from_input_value(&v), + Ok(Some([Some(1), Some(2), None])), + ); + assert_eq!( + <[[i32; 1]; 3]>::from_input_value(&v), + Err(FromInputValueArrayError::Item( + FromInputValueArrayError::Null + )), + ); + // Looks like the spec ambiguity. + // See: https://github.com/graphql/graphql-spec/pull/515 + assert_eq!( + ; 1]>; 3]>>::from_input_value(&v), + Ok(Some([Some([Some(1)]), Some([Some(2)]), None])), + ); } } diff --git a/juniper/src/validation/test_harness.rs b/juniper/src/validation/test_harness.rs index bcc0c7aa..368fcf7e 100644 --- a/juniper/src/validation/test_harness.rs +++ b/juniper/src/validation/test_harness.rs @@ -14,7 +14,7 @@ use crate::{ }, validation::{visit, MultiVisitorNil, RuleError, ValidatorContext, Visitor}, value::ScalarValue, - GraphQLInputObject, + FieldError, GraphQLInputObject, IntoFieldError, }; struct Being; @@ -616,7 +616,7 @@ impl FromInputValue for ComplexInput where S: ScalarValue, { - type Error = String; + type Error = FieldError; fn from_input_value<'a>(v: &InputValue) -> Result { let obj = v.to_object_value().ok_or("Expected object")?; @@ -644,7 +644,7 @@ where .ok_or("Expected booleanField")?, string_list_field: obj .get("stringListField") - .map(|v| v.convert()) + .map(|v| v.convert().map_err(IntoFieldError::into_field_error)) .transpose()? .ok_or("Expected stringListField")?, })