diff --git a/CHANGELOG.md b/CHANGELOG.md index 512ccb56..b15800be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.4] 2021-11-18 + +- Remove `OutputJson` because `Json` can replace it. +- Allowed use validators on wrapper types, for example: `Option`, `MaybeUnefined`. + ## [3.0.3] 2021-11-18 - [integrations] Make `GraphQLWebSocket::new` use generic stream. diff --git a/derive/src/enum.rs b/derive/src/enum.rs index 4aef3cc9..9efab5d4 100644 --- a/derive/src/enum.rs +++ b/derive/src/enum.rs @@ -148,6 +148,8 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::InputType for #ident { + type RawValueType = Self; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { Self::__type_name() } @@ -163,6 +165,10 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { fn to_value(&self) -> #crate_name::Value { #crate_name::resolver_utils::enum_value(*self) } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + ::std::option::Option::Some(self) + } } #[#crate_name::async_trait::async_trait] diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 70bf19fc..ce7317c2 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -206,6 +206,8 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult quote! { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::InputType for #ident { + type RawValueType = Self; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { ::std::borrow::Cow::Borrowed(#gql_typename) } @@ -242,6 +244,10 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult fn federation_fields() -> ::std::option::Option<::std::string::String> { #get_federation_fields } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + ::std::option::Option::Some(self) + } } impl #crate_name::InputObjectType for #ident {} @@ -295,6 +301,8 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let expanded = quote! { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::InputType for #concrete_type { + type RawValueType = Self; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { ::std::borrow::Cow::Borrowed(#gql_typename) } @@ -314,6 +322,10 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult fn federation_fields() -> ::std::option::Option<::std::string::String> { Self::__internal_federation_fields() } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + ::std::option::Option::Some(self) + } } impl #crate_name::InputObjectType for #concrete_type {} diff --git a/derive/src/newtype.rs b/derive/src/newtype.rs index 7d01d3f5..2405f89f 100644 --- a/derive/src/newtype.rs +++ b/derive/src/newtype.rs @@ -82,6 +82,8 @@ pub fn generate(newtype_args: &args::NewType) -> GeneratorResult { #[allow(clippy::all, clippy::pedantic)] impl #impl_generics #crate_name::InputType for #ident #ty_generics #where_clause { + type RawValueType = #inner_ty; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { #type_name } @@ -97,6 +99,10 @@ pub fn generate(newtype_args: &args::NewType) -> GeneratorResult { fn to_value(&self) -> #crate_name::Value { <#ident as #crate_name::ScalarType>::to_value(self) } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + self.0.as_raw_value() + } } #[allow(clippy::all, clippy::pedantic)] diff --git a/derive/src/scalar.rs b/derive/src/scalar.rs index 34998b27..a7feebfd 100644 --- a/derive/src/scalar.rs +++ b/derive/src/scalar.rs @@ -40,6 +40,8 @@ pub fn generate( #[allow(clippy::all, clippy::pedantic)] impl #generic #crate_name::InputType for #self_ty #where_clause { + type RawValueType = Self; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { ::std::borrow::Cow::Borrowed(#gql_typename) } @@ -61,6 +63,10 @@ pub fn generate( fn to_value(&self) -> #crate_name::Value { <#self_ty as #crate_name::ScalarType>::to_value(self) } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + ::std::option::Option::Some(self) + } } #[allow(clippy::all, clippy::pedantic)] diff --git a/derive/src/validators.rs b/derive/src/validators.rs index d6ccc746..bc84a646 100644 --- a/derive/src/validators.rs +++ b/derive/src/validators.rs @@ -72,111 +72,110 @@ impl Validators { map_err: Option, ) -> Result { let mut codes = Vec::new(); - let mut value = value; - let mut container = None; - - if self.list { - container = Some(quote!(#value)); - value = quote!(__item); - } if let Some(n) = &self.multiple_of { codes.push(quote! { - #crate_name::validators::multiple_of(#value, #n) + #crate_name::validators::multiple_of(__raw_value, #n) }); } if let Some(n) = &self.maximum { codes.push(quote! { - #crate_name::validators::maximum(#value, #n) + #crate_name::validators::maximum(__raw_value, #n) }); } if let Some(n) = &self.minimum { codes.push(quote! { - #crate_name::validators::minimum(#value, #n) + #crate_name::validators::minimum(__raw_value, #n) }); } if let Some(n) = &self.max_length { codes.push(quote! { - #crate_name::validators::max_length(#value, #n) + #crate_name::validators::max_length(__raw_value, #n) }); } if let Some(n) = &self.min_length { codes.push(quote! { - #crate_name::validators::min_length(#value, #n) + #crate_name::validators::min_length(__raw_value, #n) }); } if let Some(n) = &self.max_items { codes.push(quote! { - #crate_name::validators::max_items(#value, #n) + #crate_name::validators::max_items(__raw_value, #n) }); } if let Some(n) = &self.min_items { codes.push(quote! { - #crate_name::validators::min_items(#value, #n) + #crate_name::validators::min_items(__raw_value, #n) }); } if let Some(n) = &self.chars_max_length { codes.push(quote! { - #crate_name::validators::chars_max_length(#value, #n) + #crate_name::validators::chars_max_length(__raw_value, #n) }); } if let Some(n) = &self.chars_min_length { codes.push(quote! { - #crate_name::validators::chars_min_length(#value, #n) + #crate_name::validators::chars_min_length(__raw_value, #n) }); } if self.email { codes.push(quote! { - #crate_name::validators::email(#value) + #crate_name::validators::email(__raw_value) }); } if self.url { codes.push(quote! { - #crate_name::validators::url(#value) + #crate_name::validators::url(__raw_value) }); } if self.ip { codes.push(quote! { - #crate_name::validators::ip(#value) + #crate_name::validators::ip(__raw_value) }); } if let Some(re) = &self.regex { codes.push(quote! { - #crate_name::validators::regex(#value, #re) + #crate_name::validators::regex(__raw_value, #re) }); } for s in &self.custom { - let expr: Expr = + let __create_custom_validator: Expr = syn::parse_str(s).map_err(|err| Error::new(s.span(), err.to_string()))?; codes.push(quote! { - #crate_name::CustomValidator::check(&(#expr), #value) + #crate_name::CustomValidator::check(&(#__create_custom_validator), __raw_value) .map_err(|err_msg| #crate_name::InputValueError::<#ty>::custom(err_msg)) }); } let codes = codes.into_iter().map(|s| quote!(#s #map_err ?)); - if let Some(container) = container { + if self.list { Ok(quote! { - for __item in #container { - #(#codes;)* + for __item in #value { + if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(__item) { + #(#codes;)* + } } }) } else { - Ok(quote!(#(#codes;)*)) + Ok(quote! { + if let ::std::option::Option::Some(__raw_value) = #crate_name::InputType::as_raw_value(#value) { + #(#codes;)* + } + }) } } } diff --git a/src/base.rs b/src/base.rs index 3d0e5b11..8f6bc176 100644 --- a/src/base.rs +++ b/src/base.rs @@ -17,6 +17,16 @@ pub trait Description { /// Represents a GraphQL input type. pub trait InputType: Send + Sync + Sized { + /// The raw type used for validator. + /// + /// Usually it is `Self`, but the wrapper type is its internal type. + /// + /// For example: + /// + /// `i32::RawValueType` is `i32` + /// `Option::RawValueType` is `i32`. + type RawValueType; + /// Type the name. fn type_name() -> Cow<'static, str>; @@ -39,6 +49,9 @@ pub trait InputType: Send + Sync + Sized { fn federation_fields() -> Option { None } + + /// Returns a reference to the raw value. + fn as_raw_value(&self) -> Option<&Self::RawValueType>; } /// Represents a GraphQL output type. @@ -151,6 +164,8 @@ impl OutputType for Box { #[async_trait::async_trait] impl InputType for Box { + type RawValueType = T::RawValueType; + fn type_name() -> Cow<'static, str> { T::type_name() } @@ -168,6 +183,10 @@ impl InputType for Box { fn to_value(&self) -> ConstValue { T::to_value(&self) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + self.as_ref().as_raw_value() + } } #[async_trait::async_trait] @@ -191,6 +210,8 @@ impl OutputType for Arc { } impl InputType for Arc { + type RawValueType = T::RawValueType; + fn type_name() -> Cow<'static, str> { T::type_name() } @@ -208,6 +229,10 @@ impl InputType for Arc { fn to_value(&self) -> ConstValue { T::to_value(&self) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + self.as_ref().as_raw_value() + } } #[doc(hidden)] diff --git a/src/resolver_utils/scalar.rs b/src/resolver_utils/scalar.rs index b2590417..90de393a 100644 --- a/src/resolver_utils/scalar.rs +++ b/src/resolver_utils/scalar.rs @@ -147,6 +147,8 @@ macro_rules! scalar_internal { } impl $crate::InputType for $ty { + type RawValueType = Self; + fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> { ::std::borrow::Cow::Borrowed($name) } @@ -172,6 +174,10 @@ macro_rules! scalar_internal { fn to_value(&self) -> $crate::Value { <$ty as $crate::ScalarType>::to_value(self) } + + fn as_raw_value(&self) -> ::std::option::Option<&Self::RawValueType> { + ::std::option::Option::Some(self) + } } #[$crate::async_trait::async_trait] diff --git a/src/types/external/list/array.rs b/src/types/external/list/array.rs index 5b4ebef9..8237bc22 100644 --- a/src/types/external/list/array.rs +++ b/src/types/external/list/array.rs @@ -8,6 +8,8 @@ use crate::{ }; impl InputType for [T; N] { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -46,6 +48,10 @@ impl InputType for [T; N] { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/list/btree_set.rs b/src/types/external/list/btree_set.rs index cd42f5b4..2e844e2a 100644 --- a/src/types/external/list/btree_set.rs +++ b/src/types/external/list/btree_set.rs @@ -9,6 +9,8 @@ use crate::{ }; impl InputType for BTreeSet { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -40,6 +42,10 @@ impl InputType for BTreeSet { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/list/hash_set.rs b/src/types/external/list/hash_set.rs index 14571488..7a9a42c3 100644 --- a/src/types/external/list/hash_set.rs +++ b/src/types/external/list/hash_set.rs @@ -11,6 +11,8 @@ use crate::{ }; impl InputType for HashSet { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -42,6 +44,10 @@ impl InputType for HashSet { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/list/linked_list.rs b/src/types/external/list/linked_list.rs index 40f0232a..bb99011f 100644 --- a/src/types/external/list/linked_list.rs +++ b/src/types/external/list/linked_list.rs @@ -9,6 +9,8 @@ use crate::{ }; impl InputType for LinkedList { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -41,6 +43,10 @@ impl InputType for LinkedList { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/list/vec.rs b/src/types/external/list/vec.rs index dec847f0..9ab6a0ef 100644 --- a/src/types/external/list/vec.rs +++ b/src/types/external/list/vec.rs @@ -8,6 +8,8 @@ use crate::{ }; impl InputType for Vec { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -37,6 +39,10 @@ impl InputType for Vec { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/list/vec_deque.rs b/src/types/external/list/vec_deque.rs index 6e1503f5..131d27ff 100644 --- a/src/types/external/list/vec_deque.rs +++ b/src/types/external/list/vec_deque.rs @@ -9,6 +9,8 @@ use crate::{ }; impl InputType for VecDeque { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -41,6 +43,10 @@ impl InputType for VecDeque { fn to_value(&self) -> Value { Value::List(self.iter().map(InputType::to_value).collect()) } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } #[async_trait::async_trait] diff --git a/src/types/external/optional.rs b/src/types/external/optional.rs index f24f33d9..5643fe20 100644 --- a/src/types/external/optional.rs +++ b/src/types/external/optional.rs @@ -7,6 +7,8 @@ use crate::{ }; impl InputType for Option { + type RawValueType = T::RawValueType; + fn type_name() -> Cow<'static, str> { T::type_name() } @@ -35,6 +37,13 @@ impl InputType for Option { None => Value::Null, } } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + match self { + Some(value) => value.as_raw_value(), + None => None, + } + } } #[async_trait::async_trait] diff --git a/src/types/external/secrecy.rs b/src/types/external/secrecy.rs index 5a094d36..d7b29471 100644 --- a/src/types/external/secrecy.rs +++ b/src/types/external/secrecy.rs @@ -1,10 +1,12 @@ use std::borrow::Cow; -use secrecy::{Secret, Zeroize}; +use secrecy::{ExposeSecret, Secret, Zeroize}; use crate::{registry, InputType, InputValueError, InputValueResult, Value}; impl InputType for Secret { + type RawValueType = T::RawValueType; + fn type_name() -> Cow<'static, str> { T::type_name() } @@ -26,4 +28,8 @@ impl InputType for Secret { fn to_value(&self) -> Value { Value::Null } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + self.expose_secret().as_raw_value() + } } diff --git a/src/types/json.rs b/src/types/json.rs index e5b18aad..b9aa1a68 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize}; use crate::parser::types::Field; use crate::registry::{MetaType, Registry}; use crate::{ - from_value, to_value, ContextSelectionSet, InputValueError, InputValueResult, OutputType, - Positioned, Scalar, ScalarType, ServerResult, Value, + from_value, to_value, ContextSelectionSet, InputType, InputValueResult, OutputType, Positioned, + ServerResult, Value, }; /// A scalar that can represent any JSON value. @@ -32,57 +32,51 @@ impl DerefMut for Json { } } -impl From for Json { +impl From for Json { fn from(value: T) -> Self { Self(value) } } -/// A scalar that can represent any JSON value. -#[Scalar(internal, name = "JSON")] -impl ScalarType for Json { - fn parse(value: Value) -> InputValueResult { - Ok(from_value(value)?) +impl InputType for Json { + type RawValueType = T; + + fn type_name() -> Cow<'static, str> { + Cow::Borrowed("JSON") + } + + fn create_type_info(registry: &mut Registry) -> String { + registry.create_output_type::, _>(|_| MetaType::Scalar { + name: ::type_name().to_string(), + description: None, + is_valid: |_| true, + visible: None, + specified_by_url: None, + }) + } + + fn parse(value: Option) -> InputValueResult { + Ok(from_value(value.unwrap_or_default())?) } fn to_value(&self) -> Value { to_value(&self.0).unwrap_or_default() } -} -/// A `Json` type that only implements `OutputType`. -#[derive(Serialize, Clone, Debug, Eq, PartialEq, Hash, Default)] -pub struct OutputJson(pub T); - -impl Deref for OutputJson { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for OutputJson { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From for OutputJson { - fn from(value: T) -> Self { - Self(value) + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(&self.0) } } #[async_trait::async_trait] -impl OutputType for OutputJson { +impl OutputType for Json { fn type_name() -> Cow<'static, str> { - Cow::Borrowed("Json") + Cow::Borrowed("JSON") } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::, _>(|_| MetaType::Scalar { - name: Self::type_name().to_string(), + registry.create_output_type::, _>(|_| MetaType::Scalar { + name: ::type_name().to_string(), description: None, is_valid: |_| true, visible: None, @@ -99,20 +93,6 @@ impl OutputType for OutputJson { } } -/// A scalar that can represent any JSON value. -#[Scalar(internal, name = "JSON")] -impl ScalarType for serde_json::Value { - fn parse(value: Value) -> InputValueResult { - value - .into_json() - .map_err(|_| InputValueError::custom("Invalid JSON")) - } - - fn to_value(&self) -> Value { - Value::from_json(self.clone()).unwrap_or_default() - } -} - #[cfg(test)] mod test { use crate::*; @@ -152,7 +132,7 @@ mod test { } #[tokio::test] - async fn test_output_json_type() { + async fn test_json_type_for_serialize_only() { #[derive(Serialize)] struct MyStruct { a: i32, @@ -164,7 +144,7 @@ mod test { #[Object(internal)] impl Query { - async fn obj(&self) -> OutputJson { + async fn obj(&self) -> Json { MyStruct { a: 1, b: 2, diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 63cb47fb..fe99a759 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -175,6 +175,8 @@ impl MaybeUndefined { } impl InputType for MaybeUndefined { + type RawValueType = T::RawValueType; + fn type_name() -> Cow<'static, str> { T::type_name() } @@ -204,6 +206,14 @@ impl InputType for MaybeUndefined { _ => Value::Null, } } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + if let MaybeUndefined::Value(value) = self { + value.as_raw_value() + } else { + None + } + } } impl MaybeUndefined> { diff --git a/src/types/mod.rs b/src/types/mod.rs index e122e487..4fbddc39 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -20,7 +20,7 @@ pub use any::Any; pub use empty_mutation::EmptyMutation; pub use empty_subscription::EmptySubscription; pub use id::ID; -pub use json::{Json, OutputJson}; +pub use json::Json; pub use maybe_undefined::MaybeUndefined; pub use merged_object::{MergedObject, MergedObjectTail}; #[cfg(feature = "string_number")] diff --git a/src/types/upload.rs b/src/types/upload.rs index 3c41637a..e6552c29 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -101,6 +101,8 @@ impl Upload { } impl InputType for Upload { + type RawValueType = Self; + fn type_name() -> Cow<'static, str> { Cow::Borrowed("Upload") } @@ -129,4 +131,8 @@ impl InputType for Upload { fn to_value(&self) -> Value { Value::Null } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } } diff --git a/tests/json_type.rs b/tests/json_type.rs index 1f6e2c91..b69425ea 100644 --- a/tests/json_type.rs +++ b/tests/json_type.rs @@ -22,12 +22,12 @@ pub async fn test_json_scalar() { Json(MyData(items)) } - async fn data_output(&self) -> OutputJson<&MyDataOutput> { - OutputJson(&self.data) + async fn data_output(&self) -> Json<&MyDataOutput> { + Json(&self.data) } - async fn data_output_clone(&self) -> OutputJson { - OutputJson(self.data.clone()) + async fn data_output_clone(&self) -> Json { + Json(self.data.clone()) } } diff --git a/tests/validators.rs b/tests/validators.rs index 68ed1693..7c7ba424 100644 --- a/tests/validators.rs +++ b/tests/validators.rs @@ -1,5 +1,6 @@ use async_graphql::*; use futures_util::{Stream, StreamExt}; +use std::sync::Arc; #[tokio::test] pub async fn test_all_validator() { @@ -120,6 +121,8 @@ pub async fn test_validator_on_input_object_field() { struct MyInput { #[graphql(validator(maximum = 10))] a: i32, + #[graphql(validator(maximum = 10))] + b: Option, } struct Query; @@ -127,7 +130,7 @@ pub async fn test_validator_on_input_object_field() { #[Object] impl Query { async fn value(&self, input: MyInput) -> i32 { - input.a + input.a + input.b.unwrap_or_default() } } @@ -142,6 +145,17 @@ pub async fn test_validator_on_input_object_field() { value!({ "value": 5 }) ); + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema + .execute("{ value(input: {a: 5, b: 7}) }") + .await + .into_result() + .unwrap() + .data, + value!({ "value": 12 }) + ); + assert_eq!( schema .execute("{ value(input: {a: 11}) }") @@ -160,6 +174,25 @@ pub async fn test_validator_on_input_object_field() { extensions: None }] ); + + assert_eq!( + schema + .execute("{ value(input: {a: 5, b: 20}) }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "Int": the value is 20, must be less than or equal to 10 (occurred while parsing "MyInput")"# + .to_string(), + source: None, + locations: vec![Pos { + line: 1, + column: 16 + }], + path: vec![PathSegment::Field("value".to_string())], + extensions: None + }] + ); } #[tokio::test] @@ -454,3 +487,121 @@ pub async fn test_list_validator() { }] ); } + +#[tokio::test] +pub async fn test_validate_wrapper_types() { + #[derive(NewType)] + struct Size(i32); + + struct Query; + + #[Object] + impl Query { + async fn a(&self, #[graphql(validator(maximum = 10))] n: Option) -> i32 { + n.unwrap_or_default() + } + + async fn b(&self, #[graphql(validator(maximum = 10))] n: Option>) -> i32 { + n.unwrap_or_default().unwrap_or_default() + } + + async fn c(&self, #[graphql(validator(maximum = 10))] n: MaybeUndefined) -> i32 { + n.take().unwrap_or_default() + } + + async fn d(&self, #[graphql(validator(maximum = 10))] n: Box) -> i32 { + *n + } + + async fn e(&self, #[graphql(validator(maximum = 10))] n: Arc) -> i32 { + *n + } + + async fn f(&self, #[graphql(validator(maximum = 10))] n: Json) -> i32 { + n.0 + } + + async fn g(&self, #[graphql(validator(maximum = 10))] n: Option>) -> i32 { + n.map(|n| n.0).unwrap_or_default() + } + + async fn h(&self, #[graphql(validator(maximum = 10))] n: Size) -> i32 { + n.0 + } + + async fn i(&self, #[graphql(validator(list, maximum = 10))] n: Vec) -> i32 { + n.into_iter().sum() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + let successes = [ + ("{ a(n: 5) }", value!({ "a": 5 })), + ("{ a }", value!({ "a": 0 })), + ("{ b(n: 5) }", value!({ "b": 5 })), + ("{ b }", value!({ "b": 0 })), + ("{ c(n: 5) }", value!({ "c": 5 })), + ("{ c(n: null) }", value!({ "c": 0 })), + ("{ c }", value!({ "c": 0 })), + ("{ d(n: 5) }", value!({ "d": 5 })), + ("{ e(n: 5) }", value!({ "e": 5 })), + ("{ f(n: 5) }", value!({ "f": 5 })), + ("{ g(n: 5) }", value!({ "g": 5 })), + ("{ g }", value!({ "g": 0 })), + ("{ h(n: 5) }", value!({ "h": 5 })), + ("{ i(n: [1, 2, 3]) }", value!({ "i": 6 })), + ]; + + for (query, res) in successes { + assert_eq!(schema.execute(query).await.into_result().unwrap().data, res); + } + + assert_eq!( + schema + .execute("{ a(n:20) }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "Int": the value is 20, must be less than or equal to 10"# + .to_string(), + source: None, + locations: vec![Pos { line: 1, column: 7 }], + path: vec![PathSegment::Field("a".to_string())], + extensions: None + }] + ); + + assert_eq!( + schema + .execute("{ b(n:20) }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "Int": the value is 20, must be less than or equal to 10"# + .to_string(), + source: None, + locations: vec![Pos { line: 1, column: 7 }], + path: vec![PathSegment::Field("b".to_string())], + extensions: None + }] + ); + + assert_eq!( + schema + .execute("{ f(n:20) }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "Int": the value is 20, must be less than or equal to 10"# + .to_string(), + source: None, + locations: vec![Pos { line: 1, column: 7 }], + path: vec![PathSegment::Field("f".to_string())], + extensions: None + }] + ); +}