diff --git a/derive/src/args.rs b/derive/src/args.rs index cf3b437c..d9bc2530 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -6,6 +6,8 @@ use syn::{ Attribute, Generics, Ident, Lit, LitBool, LitStr, Meta, NestedMeta, Path, Type, Visibility, }; +use crate::validators::Validators; + #[derive(FromMeta, Clone)] #[darling(default)] pub struct CacheControl { @@ -151,7 +153,7 @@ pub struct SimpleObjectField { #[darling(default)] pub default_with: Option, #[darling(default)] - pub validator: Option, + pub validator: Option, #[darling(default)] pub flatten: bool, #[darling(default)] @@ -200,7 +202,7 @@ pub struct Argument { pub desc: Option, pub default: Option, pub default_with: Option, - pub validator: Option, + pub validator: Option, pub key: bool, // for entity pub visible: Option, pub secret: bool, @@ -350,7 +352,7 @@ pub struct InputObjectField { #[darling(default)] pub default_with: Option, #[darling(default)] - pub validator: Option, + pub validator: Option, #[darling(default)] pub flatten: bool, #[darling(default)] @@ -482,7 +484,7 @@ pub struct SubscriptionFieldArgument { pub desc: Option, pub default: Option, pub default_with: Option, - pub validator: Option, + pub validator: Option, pub visible: Option, pub secret: bool, } diff --git a/derive/src/complex_object.rs b/derive/src/complex_object.rs index 02e85059..f5e5731e 100644 --- a/derive/src/complex_object.rs +++ b/derive/src/complex_object.rs @@ -12,8 +12,8 @@ use syn::{ use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget}; use crate::output_type::OutputType; use crate::utils::{ - extract_input_args, gen_deprecation, generate_default, generate_guards, generate_validator, - get_cfg_attrs, get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name, + extract_input_args, gen_deprecation, generate_default, generate_guards, get_cfg_attrs, + get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name, parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult, }; @@ -205,14 +205,6 @@ pub fn generate( }) .unwrap_or_else(|| quote! {::std::option::Option::None}); - let validator = match &validator { - Some(meta) => { - let stream = generate_validator(&crate_name, meta)?; - quote!(::std::option::Option::Some(#stream)) - } - None => quote!(::std::option::Option::None), - }; - let visible = visible_fn(visible); schema_args.push(quote! { args.insert(#name, #crate_name::registry::MetaInputValue { @@ -220,7 +212,6 @@ pub fn generate( description: #desc, ty: <#ty as #crate_name::InputType>::create_type_info(registry), default_value: #schema_default, - validator: #validator, visible: #visible, is_secret: #secret, }); @@ -235,15 +226,23 @@ pub fn generate( } None => quote! { ::std::option::Option::None }, }; + + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(), + ); + // We're generating a new identifier, // so remove the 'r#` prefix if present let param_getter_name = get_param_getter_ident(&ident.ident.unraw().to_string()); get_params.push(quote! { - #[allow(non_snake_case)] - let #param_getter_name = || -> #crate_name::ServerResult<#ty> { ctx.param_value(#name, #default) }; - #[allow(non_snake_case)] - let #ident: #ty = #param_getter_name()?; - }); + #[allow(non_snake_case)] + let #param_getter_name = || -> #crate_name::ServerResult<#ty> { ctx.param_value(#name, #default) }; + #[allow(non_snake_case)] + let #ident: #ty = #param_getter_name()?; + #validators + }); } let schema_ty = ty.value_type(); diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 3d433f93..76766a35 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -5,9 +5,7 @@ use syn::ext::IdentExt; use syn::Error; use crate::args::{self, RenameRuleExt, RenameTarget}; -use crate::utils::{ - generate_default, generate_validator, get_crate_name, get_rustdoc, visible_fn, GeneratorResult, -}; +use crate::utils::{generate_default, get_crate_name, get_rustdoc, visible_fn, GeneratorResult}; pub fn generate(object_args: &args::InputObject) -> GeneratorResult { let crate_name = get_crate_name(object_args.internal); @@ -77,6 +75,16 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult federation_fields.push((ty, name.clone())); + let validators = field + .validator + .clone() + .unwrap_or_default() + .create_validators( + &crate_name, + quote!(&#ident), + quote!(.map_err(#crate_name::InputValueError::propagate)?), + ); + if field.flatten { flatten_fields.push((ident, ty)); @@ -93,6 +101,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let #ident: #ty = #crate_name::InputType::parse( ::std::option::Option::Some(#crate_name::Value::Object(::std::clone::Clone::clone(&obj))) ).map_err(#crate_name::InputValueError::propagate)?; + #validators }); fields.push(ident); @@ -105,13 +114,6 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult continue; } - let validator = match &field.validator { - Some(meta) => { - let stream = generate_validator(&crate_name, meta)?; - quote!(::std::option::Option::Some(#stream)) - } - None => quote!(::std::option::Option::None), - }; let desc = get_rustdoc(&field.attrs)? .map(|s| quote! { ::std::option::Option::Some(#s) }) .unwrap_or_else(|| quote! {::std::option::Option::None}); @@ -140,12 +142,14 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult ::std::option::Option::None => #default, } }; + #validators }); } else { get_fields.push(quote! { #[allow(non_snake_case)] let #ident: #ty = #crate_name::InputType::parse(obj.get(#name).cloned()) .map_err(#crate_name::InputValueError::propagate)?; + #validators }); } @@ -164,7 +168,6 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult description: #desc, ty: <#ty as #crate_name::InputType>::create_type_info(registry), default_value: #schema_default, - validator: #validator, visible: #visible, is_secret: #secret, }); diff --git a/derive/src/interface.rs b/derive/src/interface.rs index 49cca15d..d372f9b2 100644 --- a/derive/src/interface.rs +++ b/derive/src/interface.rs @@ -228,7 +228,6 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult::create_type_info(registry), default_value: #schema_default, - validator: ::std::option::Option::None, visible: #visible, is_secret: #secret, }); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index c9d37cbf..5d1d10e9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -20,6 +20,7 @@ mod simple_object; mod subscription; mod union; mod utils; +mod validators; use darling::{FromDeriveInput, FromMeta}; use proc_macro::TokenStream; diff --git a/derive/src/object.rs b/derive/src/object.rs index 1cd409e9..0a78cee5 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -12,8 +12,8 @@ use syn::{ use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget}; use crate::output_type::OutputType; use crate::utils::{ - extract_input_args, gen_deprecation, generate_default, generate_guards, generate_validator, - get_cfg_attrs, get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name, + extract_input_args, gen_deprecation, generate_default, generate_guards, get_cfg_attrs, + get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name, parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult, }; @@ -340,14 +340,6 @@ pub fn generate( }) .unwrap_or_else(|| quote! {::std::option::Option::None}); - let validator = match &validator { - Some(meta) => { - let stream = generate_validator(&crate_name, meta)?; - quote!(::std::option::Option::Some(#stream)) - } - None => quote!(::std::option::Option::None), - }; - let visible = visible_fn(visible); schema_args.push(quote! { args.insert(#name, #crate_name::registry::MetaInputValue { @@ -355,7 +347,6 @@ pub fn generate( description: #desc, ty: <#ty as #crate_name::InputType>::create_type_info(registry), default_value: #schema_default, - validator: #validator, visible: #visible, is_secret: #secret, }); @@ -370,6 +361,13 @@ pub fn generate( } None => quote! { ::std::option::Option::None }, }; + + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(), + ); + // We're generating a new identifier, // so remove the 'r#` prefix if present let param_getter_name = @@ -379,6 +377,7 @@ pub fn generate( let #param_getter_name = || -> #crate_name::ServerResult<#ty> { ctx.param_value(#name, #default) }; #[allow(non_snake_case)] let #ident: #ty = #param_getter_name()?; + #validators }); } diff --git a/derive/src/subscription.rs b/derive/src/subscription.rs index daa1116b..90c76531 100644 --- a/derive/src/subscription.rs +++ b/derive/src/subscription.rs @@ -9,9 +9,9 @@ use syn::{ use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget, SubscriptionField}; use crate::output_type::OutputType; use crate::utils::{ - gen_deprecation, generate_default, generate_guards, generate_validator, get_cfg_attrs, - get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name, - parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult, + gen_deprecation, generate_default, generate_guards, get_cfg_attrs, get_crate_name, + get_param_getter_ident, get_rustdoc, get_type_path_and_name, parse_complexity_expr, + parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult, }; pub fn generate( @@ -167,14 +167,6 @@ pub fn generate( .unwrap_or_else(|| quote! {::std::option::Option::None}); let default = generate_default(default, default_with)?; - let validator = match &validator { - Some(meta) => { - let stream = generate_validator(&crate_name, meta)?; - quote!(::std::option::Option::Some(#stream)) - } - None => quote!(::std::option::Option::None), - }; - let schema_default = default .as_ref() .map(|value| { @@ -193,7 +185,6 @@ pub fn generate( description: #desc, ty: <#ty as #crate_name::InputType>::create_type_info(registry), default_value: #schema_default, - validator: #validator, visible: #visible, is_secret: #secret, }); @@ -205,12 +196,18 @@ pub fn generate( Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) }, None => quote! { ::std::option::Option::None }, }; + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(), + ); let param_getter_name = get_param_getter_ident(&ident.ident.to_string()); get_params.push(quote! { #[allow(non_snake_case)] let #param_getter_name = || -> #crate_name::ServerResult<#ty> { ctx.param_value(#name, #default) }; #[allow(non_snake_case)] let #ident: #ty = ctx.param_value(#name, #default)?; + #validators }); } diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 394a30d4..66ef59ab 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -46,105 +46,6 @@ pub fn get_crate_name(internal: bool) -> TokenStream { } } -fn generate_nested_validator( - crate_name: &TokenStream, - nested_meta: &NestedMeta, -) -> GeneratorResult { - let mut params = Vec::new(); - match nested_meta { - NestedMeta::Meta(Meta::List(ls)) => { - if ls.path.is_ident("and") { - let mut validators = Vec::new(); - for nested_meta in &ls.nested { - validators.push(generate_nested_validator(crate_name, nested_meta)?); - } - Ok(validators - .into_iter() - .fold(None, |acc, item| match acc { - Some(prev) => Some(quote! { #crate_name::validators::InputValueValidatorExt::and(#prev, #item) }), - None => Some(item), - }) - .unwrap()) - } else if ls.path.is_ident("or") { - let mut validators = Vec::new(); - for nested_meta in &ls.nested { - validators.push(generate_nested_validator(crate_name, nested_meta)?); - } - Ok(validators - .into_iter() - .fold(None, |acc, item| match acc { - Some(prev) => Some(quote! { #crate_name::validators::InputValueValidatorExt::or(#prev, #item) }), - None => Some(item), - }) - .unwrap()) - } else if ls.path.is_ident("list") { - if ls.nested.len() > 1 { - return Err(Error::new_spanned( - ls, - "Only one validator can be wrapped with list.", - ) - .into()); - } - if ls.nested.is_empty() { - return Err( - Error::new_spanned(ls, "At least one validator must be defined").into(), - ); - } - let validator = generate_nested_validator(crate_name, &ls.nested[0])?; - Ok(quote! { - #crate_name::validators::List(#validator) - }) - } else { - let ty = &ls.path; - for item in &ls.nested { - if let NestedMeta::Meta(Meta::NameValue(nv)) = item { - let name = &nv.path; - if let Lit::Str(value) = &nv.lit { - let expr = syn::parse_str::(&value.value())?; - params.push(quote! { #name: (#expr).into() }); - } else { - return Err(Error::new_spanned( - &nv.lit, - "Value must be string literal", - ) - .into()); - } - } else { - return Err(Error::new_spanned( - nested_meta, - "Invalid property for validator", - ) - .into()); - } - } - Ok(quote! { #ty { #(#params),* } }) - } - } - NestedMeta::Meta(Meta::Path(ty)) => Ok(quote! { #ty {} }), - NestedMeta::Meta(Meta::NameValue(_)) | NestedMeta::Lit(_) => { - Err(Error::new_spanned(nested_meta, "Invalid validator").into()) - } - } -} - -pub fn generate_validator(crate_name: &TokenStream, args: &Meta) -> GeneratorResult { - match args { - Meta::List(args) => { - if args.nested.len() > 1 { - return Err(Error::new_spanned(args, "Only one validator can be defined. You can connect combine validators with `and` or `or`").into()); - } - if args.nested.is_empty() { - return Err( - Error::new_spanned(args, "At least one validator must be defined").into(), - ); - } - let validator = generate_nested_validator(crate_name, &args.nested[0])?; - Ok(quote! { ::std::sync::Arc::new(#validator) }) - } - _ => Err(Error::new_spanned(args, "Invalid validator").into()), - } -} - pub fn generate_guards( crate_name: &TokenStream, args: &Meta, diff --git a/derive/src/validators.rs b/derive/src/validators.rs new file mode 100644 index 00000000..2c6fdba2 --- /dev/null +++ b/derive/src/validators.rs @@ -0,0 +1,78 @@ +use darling::FromMeta; +use proc_macro2::TokenStream; +use quote::quote; + +#[derive(FromMeta, Default, Clone)] +pub struct Validators { + #[darling(default)] + multiple_of: Option, + #[darling(default)] + maximum: Option, + #[darling(default)] + minimum: Option, + #[darling(default)] + max_length: Option, + #[darling(default)] + min_length: Option, + #[darling(default)] + max_items: Option, + #[darling(default)] + min_items: Option, + #[darling(default, multiple)] + custom: Vec, +} + +impl Validators { + pub fn create_validators( + &self, + crate_name: &TokenStream, + value: TokenStream, + map_err: TokenStream, + ) -> TokenStream { + let mut codes = Vec::new(); + + if let Some(n) = &self.multiple_of { + codes.push(quote! { + #crate_name::validators::multiple_of(#value, #n) #map_err + }); + } + + if let Some(n) = &self.maximum { + codes.push(quote! { + #crate_name::validators::maximum(#value, #n) #map_err + }); + } + + if let Some(n) = &self.minimum { + codes.push(quote! { + #crate_name::validators::minimum(#value, #n) #map_err + }); + } + + if let Some(n) = &self.max_length { + codes.push(quote! { + #crate_name::validators::max_length(#value, #n) #map_err + }); + } + + if let Some(n) = &self.min_length { + codes.push(quote! { + #crate_name::validators::min_length(#value, #n) #map_err + }); + } + + if let Some(n) = &self.max_items { + codes.push(quote! { + #crate_name::validators::max_items(#value, #n) #map_err + }); + } + + if let Some(n) = &self.min_items { + codes.push(quote! { + #crate_name::validators::min_items(#value, #n) #map_err + }); + } + + quote!(#(#codes;)*) + } +} diff --git a/src/lib.rs b/src/lib.rs index e7982c1c..b330e793 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,6 +179,7 @@ pub mod guard; pub mod http; pub mod resolver_utils; pub mod types; +#[doc(hidden)] pub mod validators; #[doc(hidden)] diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 24b84524..5d9aa982 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -3,7 +3,6 @@ mod export_sdl; mod stringify_exec_doc; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::sync::Arc; use indexmap::map::IndexMap; use indexmap::set::IndexSet; @@ -11,7 +10,6 @@ use indexmap::set::IndexSet; use crate::parser::types::{ BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition, }; -use crate::validators::InputValueValidator; use crate::{ model, Any, Context, InputType, OutputType, Positioned, ServerResult, SubscriptionType, Value, VisitorContext, @@ -109,7 +107,6 @@ pub struct MetaInputValue { pub description: Option<&'static str>, pub ty: String, pub default_value: Option, - pub validator: Option>, pub visible: Option, pub is_secret: bool, } @@ -615,7 +612,6 @@ impl Registry { description: None, ty: "[_Any!]!".to_string(), default_value: None, - validator: None, visible: None, is_secret: false, }, diff --git a/src/schema.rs b/src/schema.rs index 5968ae09..4544e300 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -283,7 +283,6 @@ where description: Some("Included when true."), ty: "Boolean!".to_string(), default_value: None, - validator: None, visible: None, is_secret: false, }); @@ -306,7 +305,6 @@ where description: Some("Skipped when true."), ty: "Boolean!".to_string(), default_value: None, - validator: None, visible: None, is_secret: false, }); diff --git a/src/types/query_root.rs b/src/types/query_root.rs index bd4193e7..de6c722b 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -129,7 +129,6 @@ impl OutputType for QueryRoot { description: None, ty: "String!".to_string(), default_value: None, - validator: None, visible: None, is_secret: false, }, diff --git a/src/validation/rules/arguments_of_correct_type.rs b/src/validation/rules/arguments_of_correct_type.rs index 749692d7..4bec68fe 100644 --- a/src/validation/rules/arguments_of_correct_type.rs +++ b/src/validation/rules/arguments_of_correct_type.rs @@ -55,20 +55,7 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { }) .ok(); - if let Some(validator) = &arg.validator { - if let Some(value) = &value { - if let Err(e) = validator.is_valid_with_extensions(value) { - ctx.report_error_with_extensions( - vec![name.pos], - format!("Invalid value for argument \"{}\", {}", arg.name, e.message), - e.extensions, - ); - return; - } - } - } - - if let Some(e) = value.and_then(|value| { + if let Some(reason) = value.and_then(|value| { is_valid_input_value( ctx.registry, &arg.ty, @@ -79,10 +66,9 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { }, ) }) { - ctx.report_error_with_extensions( + ctx.report_error( vec![name.pos], - format!("Invalid value for argument {}", e.message), - e.extensions, + format!("Invalid value for argument {}", reason), ); } } diff --git a/src/validation/rules/default_values_of_correct_type.rs b/src/validation/rules/default_values_of_correct_type.rs index 2e10783d..1b14181e 100644 --- a/src/validation/rules/default_values_of_correct_type.rs +++ b/src/validation/rules/default_values_of_correct_type.rs @@ -18,7 +18,7 @@ impl<'a> Visitor<'a> for DefaultValuesOfCorrectType { "Argument \"{}\" has type \"{}\" and is not nullable, so it can't have a default value", variable_definition.node.name, variable_definition.node.var_type, )); - } else if let Some(e) = is_valid_input_value( + } else if let Some(reason) = is_valid_input_value( ctx.registry, &variable_definition.node.var_type.to_string(), &value.node, @@ -27,10 +27,9 @@ impl<'a> Visitor<'a> for DefaultValuesOfCorrectType { segment: QueryPathSegment::Name(&variable_definition.node.name.node), }, ) { - ctx.report_error_with_extensions( + ctx.report_error( vec![variable_definition.pos], - format!("Invalid default value for argument {}", e.message), - e.extensions, + format!("Invalid default value for argument: {}", reason), ) } } diff --git a/src/validation/rules/no_fragment_cycles.rs b/src/validation/rules/no_fragment_cycles.rs index cbdac8db..2ae3b9ba 100644 --- a/src/validation/rules/no_fragment_cycles.rs +++ b/src/validation/rules/no_fragment_cycles.rs @@ -34,7 +34,6 @@ impl<'a> CycleDetector<'a> { self.errors.push(RuleError::new( vec![err_pos], format!("Cannot spread fragment \"{}\"", name), - None, )); } else if !self.visited.contains(name) { path.push((name, *pos)); diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 843323c8..5f1051c9 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -3,7 +3,6 @@ use std::collections::HashSet; use async_graphql_value::{ConstValue, Value}; use crate::context::QueryPathNode; -use crate::error::Error; use crate::{registry, QueryPathSegment}; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -42,12 +41,13 @@ pub fn is_valid_input_value( type_name: &str, value: &ConstValue, path_node: QueryPathNode, -) -> Option { +) -> Option { match registry::MetaTypeName::create(type_name) { registry::MetaTypeName::NonNull(type_name) => match value { - ConstValue::Null => { - Some(valid_error(&path_node, format!("expected type \"{}\"", type_name)).into()) - } + ConstValue::Null => Some(valid_error( + &path_node, + format!("expected type \"{}\"", type_name), + )), _ => is_valid_input_value(registry, type_name, value, path_node), }, registry::MetaTypeName::List(type_name) => match value { @@ -75,10 +75,10 @@ pub fn is_valid_input_value( if is_valid(&value) { None } else { - Some( - valid_error(&path_node, format!("expected type \"{}\"", type_name)) - .into(), - ) + Some(valid_error( + &path_node, + format!("expected type \"{}\"", type_name), + )) } } registry::MetaType::Enum { @@ -88,39 +88,34 @@ pub fn is_valid_input_value( } => match value { ConstValue::Enum(name) => { if !enum_values.contains_key(name.as_str()) { - Some( - valid_error( - &path_node, - format!( - "enumeration type \"{}\" does not contain the value \"{}\"", - enum_name, name - ), - ) - .into(), - ) + Some(valid_error( + &path_node, + format!( + "enumeration type \"{}\" does not contain the value \"{}\"", + enum_name, name + ), + )) } else { None } } ConstValue::String(name) => { if !enum_values.contains_key(name.as_str()) { - Some( - valid_error( - &path_node, - format!( - "enumeration type \"{}\" does not contain the value \"{}\"", - enum_name, name - ), - ) - .into(), - ) + Some(valid_error( + &path_node, + format!( + "enumeration type \"{}\" does not contain the value \"{}\"", + enum_name, name + ), + )) } else { None } } - _ => Some( - valid_error(&path_node, format!("expected type \"{}\"", type_name)).into(), - ), + _ => Some(valid_error( + &path_node, + format!("expected type \"{}\"", type_name), + )), }, registry::MetaType::InputObject { input_fields, @@ -134,19 +129,6 @@ pub fn is_valid_input_value( for field in input_fields.values() { input_names.remove(field.name); if let Some(value) = values.get(field.name) { - if let Some(validator) = &field.validator { - if let Err(mut e) = validator.is_valid_with_extensions(value) { - e.message = valid_error( - &QueryPathNode { - parent: Some(&path_node), - segment: QueryPathSegment::Name(field.name), - }, - e.message, - ); - return Some(e); - } - } - if let Some(reason) = is_valid_input_value( registry, &field.ty, @@ -161,30 +143,21 @@ pub fn is_valid_input_value( } else if registry::MetaTypeName::create(&field.ty).is_non_null() && field.default_value.is_none() { - return Some( - valid_error( - &path_node, - format!( + return Some(valid_error( + &path_node, + format!( "field \"{}\" of type \"{}\" is required but not provided", field.name, object_name, ), - ) - .into(), - ); + )); } } if let Some(name) = input_names.iter().next() { - return Some( - valid_error( - &path_node, - format!( - "unknown field \"{}\" of type \"{}\"", - name, object_name - ), - ) - .into(), - ); + return Some(valid_error( + &path_node, + format!("unknown field \"{}\" of type \"{}\"", name, object_name), + )); } None diff --git a/src/validation/visitor.rs b/src/validation/visitor.rs index 96c37717..e202944e 100644 --- a/src/validation/visitor.rs +++ b/src/validation/visitor.rs @@ -8,9 +8,7 @@ use crate::parser::types::{ OperationDefinition, OperationType, Selection, SelectionSet, TypeCondition, VariableDefinition, }; use crate::registry::{self, MetaType, MetaTypeName}; -use crate::{ - ErrorExtensionValues, InputType, Name, Pos, Positioned, ServerError, ServerResult, Variables, -}; +use crate::{InputType, Name, Pos, Positioned, ServerError, ServerResult, Variables}; #[doc(hidden)] pub struct VisitorContext<'a> { @@ -39,16 +37,7 @@ impl<'a> VisitorContext<'a> { } pub(crate) fn report_error>(&mut self, locations: Vec, msg: T) { - self.errors.push(RuleError::new(locations, msg, None)); - } - - pub(crate) fn report_error_with_extensions>( - &mut self, - locations: Vec, - msg: T, - extensions: Option, - ) { - self.errors.push(RuleError::new(locations, msg, extensions)); + self.errors.push(RuleError::new(locations, msg)); } pub(crate) fn append_errors(&mut self, errors: Vec) { @@ -809,19 +798,13 @@ fn visit_inline_fragment<'a, V: Visitor<'a>>( pub(crate) struct RuleError { pub(crate) locations: Vec, pub(crate) message: String, - pub(crate) extensions: Option, } impl RuleError { - pub(crate) fn new( - locations: Vec, - msg: impl Into, - extensions: Option, - ) -> Self { + pub(crate) fn new(locations: Vec, msg: impl Into) -> Self { Self { locations, message: msg.into(), - extensions, } } } @@ -854,7 +837,7 @@ impl From for ServerError { source: None, locations: e.locations, path: Vec::new(), - extensions: e.extensions, + extensions: None, } } } diff --git a/src/validators/int_validators.rs b/src/validators/int_validators.rs deleted file mode 100644 index 2f5a7169..00000000 --- a/src/validators/int_validators.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::validators::InputValueValidator; -use crate::Value; - -/// Integer range validator -pub struct IntRange { - /// Minimum value, including this value. - pub min: i64, - - /// Maximum value, including this value. - pub max: i64, -} - -impl InputValueValidator for IntRange { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n < self.min || n > self.max { - return Err(format!( - "the value is {}, must be between {} and {}", - n, self.min, self.max - )); - } - } - } - Ok(()) - } -} - -/// Integer less than validator -pub struct IntLessThan { - /// Less than this value. - pub value: i64, -} - -impl InputValueValidator for IntLessThan { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n >= self.value { - return Err(format!( - "the value is {}, must be less than {}", - n, self.value - )); - } - } - } - Ok(()) - } -} - -/// Integer greater than validator -pub struct IntGreaterThan { - /// Greater than this value. - pub value: i64, -} - -impl InputValueValidator for IntGreaterThan { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n <= self.value { - return Err(format!( - "the value is {}, must be greater than {}", - n, self.value - )); - } - } - } - Ok(()) - } -} - -/// Integer nonzero validator -pub struct IntNonZero {} - -impl InputValueValidator for IntNonZero { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n == 0 { - return Err(format!("the value is {}, must be nonzero", n)); - } - } - } - Ok(()) - } -} - -/// Integer equal validator -pub struct IntEqual { - /// equal this value. - pub value: i64, -} - -impl InputValueValidator for IntEqual { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n != self.value { - return Err(format!( - "the value is {}, must be equal to {}", - n, self.value - )); - } - } - } - Ok(()) - } -} diff --git a/src/validators/list_validators.rs b/src/validators/list_validators.rs deleted file mode 100644 index 0a063401..00000000 --- a/src/validators/list_validators.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::validators::InputValueValidator; -use crate::Value; - -/// List minimum length validator -pub struct ListMinLength { - /// Must be greater than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for ListMinLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::List(values) = value { - if values.len() < self.length as usize { - Err(format!( - "the value length is {}, must be greater than or equal to {}", - values.len(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -/// List maximum length validator -pub struct ListMaxLength { - /// Must be less than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for ListMaxLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::List(values) = value { - if values.len() > self.length as usize { - Err(format!( - "the value length is {}, must be less than or equal to {}", - values.len(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -#[doc(hidden)] -pub struct List(pub T); - -impl InputValueValidator for List { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::List(elems) = value { - for elem in elems { - self.0.is_valid(elem)?; - } - } - Ok(()) - } -} diff --git a/src/validators/max_items.rs b/src/validators/max_items.rs new file mode 100644 index 00000000..45716433 --- /dev/null +++ b/src/validators/max_items.rs @@ -0,0 +1,19 @@ +use std::ops::Deref; + +use crate::{InputType, InputValueError}; + +pub async fn max_items + InputType, E>( + value: &T, + len: usize, +) -> Result<(), InputValueError> { + if value.deref().len() <= len { + Ok(()) + } else { + Err(format!( + "the value length is {}, must be less than or equal to {}", + value.deref().len(), + len + ) + .into()) + } +} diff --git a/src/validators/max_length.rs b/src/validators/max_length.rs new file mode 100644 index 00000000..72e9e3be --- /dev/null +++ b/src/validators/max_length.rs @@ -0,0 +1,17 @@ +use crate::{InputType, InputValueError}; + +pub async fn max_length + InputType>( + value: &T, + len: usize, +) -> Result<(), InputValueError> { + if value.as_ref().len() <= len { + Ok(()) + } else { + Err(format!( + "the string length is {}, must be less than or equal to {}", + value.as_ref().len(), + len + ) + .into()) + } +} diff --git a/src/validators/maximum.rs b/src/validators/maximum.rs new file mode 100644 index 00000000..f1fb5c3e --- /dev/null +++ b/src/validators/maximum.rs @@ -0,0 +1,19 @@ +use num_traits::AsPrimitive; + +use crate::{InputType, InputValueError}; + +pub async fn maximum + InputType>( + value: &T, + n: f64, +) -> Result<(), InputValueError> { + if value.as_() <= n { + Ok(()) + } else { + Err(format!( + "the value is {}, must be less than or equal to {}", + value.as_(), + n + ) + .into()) + } +} diff --git a/src/validators/min_items.rs b/src/validators/min_items.rs new file mode 100644 index 00000000..d52be684 --- /dev/null +++ b/src/validators/min_items.rs @@ -0,0 +1,19 @@ +use std::ops::Deref; + +use crate::{InputType, InputValueError}; + +pub async fn min_items + InputType, E>( + value: &T, + len: usize, +) -> Result<(), InputValueError> { + if value.deref().len() >= len { + Ok(()) + } else { + Err(format!( + "the value length is {}, must be greater than or equal to {}", + value.deref().len(), + len + ) + .into()) + } +} diff --git a/src/validators/min_length.rs b/src/validators/min_length.rs new file mode 100644 index 00000000..dcd6124f --- /dev/null +++ b/src/validators/min_length.rs @@ -0,0 +1,17 @@ +use crate::{InputType, InputValueError}; + +pub async fn min_length + InputType>( + value: &T, + len: usize, +) -> Result<(), InputValueError> { + if value.as_ref().len() >= len { + Ok(()) + } else { + Err(format!( + "the string length is {}, must be greater than or equal to {}", + value.as_ref().len(), + len + ) + .into()) + } +} diff --git a/src/validators/minimum.rs b/src/validators/minimum.rs new file mode 100644 index 00000000..42595570 --- /dev/null +++ b/src/validators/minimum.rs @@ -0,0 +1,19 @@ +use num_traits::AsPrimitive; + +use crate::{InputType, InputValueError}; + +pub async fn minimum + InputType>( + value: &T, + n: f64, +) -> Result<(), InputValueError> { + if value.as_() >= n { + Ok(()) + } else { + Err(format!( + "the value is {}, must be greater than or equal to {}", + value.as_(), + n + ) + .into()) + } +} diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 5e2062c3..e556c2e8 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -1,171 +1,15 @@ -//! Input value validators +mod max_items; +mod max_length; +mod maximum; +mod min_items; +mod min_length; +mod minimum; +mod multiple_of; -mod int_validators; -mod list_validators; -mod string_validators; - -use crate::{Error, Value}; - -pub use int_validators::{IntEqual, IntGreaterThan, IntLessThan, IntNonZero, IntRange}; -pub use list_validators::{List, ListMaxLength, ListMinLength}; -pub use string_validators::{ - CharsMaxLength, CharsMinLength, Email, StringMaxLength, StringMinLength, MAC, -}; - -/// Input value validator -/// -/// You can create your own input value validator by implementing this trait. -/// -/// # Examples -/// -/// ```no_run -/// use async_graphql::*; -/// use async_graphql::validators::{Email, MAC, IntRange}; -/// -/// struct QueryRoot; -/// -/// #[Object] -/// impl QueryRoot { -/// // Input is email address -/// async fn value1(&self, #[graphql(validator(Email))] email: String) -> i32 { -/// unimplemented!() -/// } -/// -/// // Input is email or MAC address -/// async fn value2(&self, #[graphql(validator(or(Email, MAC(colon = "false"))))] email_or_mac: String) -> i32 { -/// unimplemented!() -/// } -/// -/// // Input is integer between 100 and 200 -/// async fn value3(&self, #[graphql(validator(IntRange(min = "100", max = "200")))] value: i32) -> i32 { -/// unimplemented!() -/// } -/// } -/// ``` -pub trait InputValueValidator -where - Self: Sync + Send, -{ - /// Check value is valid, returns the reason for the error if it fails, otherwise None. - /// - /// If the input type is different from the required type, return `Ok(())` directly, and other validators will find this error. - fn is_valid(&self, _value: &Value) -> Result<(), String> { - Ok(()) - } - - /// Check value is valid, returns the reason include extensions for the error if it fails, otherwise None. - /// - /// If the input type is different from the required type, return `Ok(())` directly, and other validators will find this error. - /// - /// # Examples: - /// - /// ```no_run - /// use async_graphql::validators::InputValueValidator; - /// use async_graphql::{Value, Error, ErrorExtensions}; - /// - /// pub struct IntGreaterThanZero; - /// - /// impl InputValueValidator for IntGreaterThanZero { - /// fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - /// if let Value::Number(n) = value { - /// if let Some(n) = n.as_i64() { - /// if n <= 0 { - /// return Err( - /// Error::new("Value must be greater than 0").extend_with(|_, e| e.set("code", 400)) - /// ); - /// } - /// } - /// } - /// Ok(()) - /// } - /// } - /// ``` - fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - // By default, use is_valid method to keep compatible with previous version - self.is_valid(value).map_err(Error::new) - } -} - -/// An extension trait for `InputValueValidator` -pub trait InputValueValidatorExt: InputValueValidator + Sized { - /// Merge the two validators and return None only if both validators are successful. - fn and(self, other: R) -> And { - And(self, other) - } - - /// Merge two validators, and return None when either validator verifies successfully. - fn or(self, other: R) -> Or { - Or(self, other) - } - - /// Changes the error message - fn map_err String>(self, f: F) -> MapErr { - MapErr(self, f) - } - - /// Changes the error struct - fn map_err_ext Error>(self, f: F) -> MapErrExt { - MapErrExt(self, f) - } -} - -impl InputValueValidatorExt for I {} - -/// Invalidator for `InputValueValidatorExt::and` -pub struct And(A, B); - -impl InputValueValidator for And -where - A: InputValueValidator, - B: InputValueValidator, -{ - fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - // By default, use is_valid method to keep compatible with previous version - self.0.is_valid_with_extensions(value)?; - self.1.is_valid_with_extensions(value) - } -} - -/// Invalidator for `InputValueValidator::or` -pub struct Or(A, B); - -impl InputValueValidator for Or -where - A: InputValueValidator, - B: InputValueValidator, -{ - fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - // By default, use is_valid method to keep compatible with previous version - if self.0.is_valid_with_extensions(value).is_err() { - self.1.is_valid_with_extensions(value) - } else { - Ok(()) - } - } -} - -/// Invalidator for `InputValueValidator::map_err` -pub struct MapErr(I, F); - -impl InputValueValidator for MapErr -where - I: InputValueValidator, - F: Fn(String) -> String + Send + Sync, -{ - fn is_valid(&self, value: &Value) -> Result<(), String> { - self.0.is_valid(value).map_err(&self.1) - } -} - -/// Invalidator for `InputValueValidator::map_err_ext` -pub struct MapErrExt(I, F); - -impl InputValueValidator for MapErrExt -where - I: InputValueValidator, - F: Fn(Error) -> Error + Send + Sync, -{ - fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - self.0.is_valid_with_extensions(value).map_err(&self.1) - } -} +pub use max_items::max_items; +pub use max_length::max_length; +pub use maximum::maximum; +pub use min_items::min_items; +pub use min_length::min_length; +pub use minimum::minimum; +pub use multiple_of::multiple_of; diff --git a/src/validators/multiple_of.rs b/src/validators/multiple_of.rs new file mode 100644 index 00000000..fa2e7259 --- /dev/null +++ b/src/validators/multiple_of.rs @@ -0,0 +1,14 @@ +use num_traits::AsPrimitive; + +use crate::{InputType, InputValueError}; + +pub async fn multiple_of + InputType>( + value: &T, + n: f64, +) -> Result<(), InputValueError> { + if value.as_() % n as f64 == 0.0 { + Ok(()) + } else { + Err(format!("the value must be a multiple of {}.", n).into()) + } +} diff --git a/src/validators/string_validators.rs b/src/validators/string_validators.rs deleted file mode 100644 index c48b39b7..00000000 --- a/src/validators/string_validators.rs +++ /dev/null @@ -1,153 +0,0 @@ -use once_cell::sync::Lazy; -use regex::Regex; - -use crate::validators::InputValueValidator; -use crate::Value; - -/// String minimum length validator -pub struct StringMinLength { - /// Must be greater than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for StringMinLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if s.len() < self.length as usize { - Err(format!( - "the value length is {}, must be greater than or equal to {}", - s.len(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -/// String maximum length validator -pub struct StringMaxLength { - /// Must be less than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for StringMaxLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if s.len() > self.length as usize { - Err(format!( - "the value length is {}, must be less than or equal to {}", - s.len(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -/// Chars in string minimum length validator. -pub struct CharsMinLength { - /// Must be greater than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for CharsMinLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if s.chars().count() < self.length as usize { - Err(format!( - "the value chars count is {}, must be greater than or equal to {}", - s.chars().count(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -/// Chars in string maximum length validator. -pub struct CharsMaxLength { - /// Must be less than or equal to this value. - pub length: i32, -} - -impl InputValueValidator for CharsMaxLength { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if s.chars().count() > self.length as usize { - Err(format!( - "the value chars count is {}, must be less than or equal to {}", - s.chars().count(), - self.length - )) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -static EMAIL_RE: Lazy = Lazy::new(|| { - Regex::new("^(([0-9A-Za-z!#$%&'*+-/=?^_`{|}~&&[^@]]+)|(\"([0-9A-Za-z!#$%&'*+-/=?^_`{|}~ \"(),:;<>@\\[\\\\\\]]+)\"))@").unwrap() -}); - -/// Email validator -pub struct Email {} - -impl InputValueValidator for Email { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if !EMAIL_RE.is_match(s) { - Err("invalid email format".to_string()) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} - -static MAC_ADDRESS_RE: Lazy = - Lazy::new(|| Regex::new("^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$").unwrap()); -static MAC_ADDRESS_NO_COLON_RE: Lazy = - Lazy::new(|| Regex::new("^[0-9a-fA-F]{12}$").unwrap()); - -/// MAC address validator -pub struct MAC { - /// Must include colon. - pub colon: bool, -} - -impl InputValueValidator for MAC { - fn is_valid(&self, value: &Value) -> Result<(), String> { - if let Value::String(s) = value { - if self.colon { - if !MAC_ADDRESS_RE.is_match(s) { - Err("invalid MAC format".to_string()) - } else { - Ok(()) - } - } else if !MAC_ADDRESS_NO_COLON_RE.is_match(s) { - Err("invalid MAC format".to_string()) - } else { - Ok(()) - } - } else { - Ok(()) - } - } -} diff --git a/tests/input_validators.rs b/tests/input_validators.rs deleted file mode 100644 index 6cbd9743..00000000 --- a/tests/input_validators.rs +++ /dev/null @@ -1,2150 +0,0 @@ -use async_graphql::validators::{ - CharsMaxLength, CharsMinLength, Email, InputValueValidator, IntEqual, IntGreaterThan, - IntLessThan, IntNonZero, IntRange, ListMaxLength, ListMinLength, StringMaxLength, - StringMinLength, MAC, -}; -use async_graphql::*; - -#[tokio::test] -pub async fn test_input_validator_string_min_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputMaxLength { - #[graphql(validator(StringMinLength(length = "6")))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(StringMinLength(length = "6")))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = [ - "abc", - "acbce", - "abcdef", - "abcdefghi", - "abcdefghijkl", - "abcdefghijklmnop", - ]; - - let validator_length = 6; - for case in &test_cases { - let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); - let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); - let case_length = case.len(); - - if case_length < validator_length { - let should_fail_msg = format!( - "StringMinValue case {} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value length is {}, must be greater than or equal to {}", - case_length, validator_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value length is {}, must be greater than or equal to {}", - case_length, validator_length - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with StringMinLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with StringMinLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_string_max_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputMaxLength { - #[graphql(validator(StringMaxLength(length = "6")))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(StringMaxLength(length = "6")))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = [ - "abc", - "acbce", - "abcdef", - "abcdefghi", - "abcdefghijkl", - "abcdefghijklmnop", - ]; - - let validator_length = 6; - for case in &test_cases { - let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); - let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); - let case_length = case.len(); - - if case_length > validator_length { - let should_fail_msg = format!( - "StringMaxValue case {} should have failed, but did not", - case - ); - - let field_error_msg = format!("Invalid value for argument \"id\", the value length is {}, must be less than or equal to {}", case_length, validator_length); - let object_error_msg = format!("Invalid value for argument \"input.id\", the value length is {}, must be less than or equal to {}", case_length, validator_length); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with StringMaxLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with StringMaxLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_chars_min_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputMaxLength { - #[graphql(validator(CharsMinLength(length = "6")))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(CharsMinLength(length = "6")))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = ["一2三4五", "壹2叁4伍6", "一2三4五6七", "壹2叁4伍6Ⅶ8"]; - - let validator_length = 6; - for case in &test_cases { - let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); - let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); - let case_length = case.chars().count(); - - if case_length < validator_length { - let should_fail_msg = format!( - "CharsMinLength case {} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value chars count is {}, must be greater than or equal to {}", - case_length, validator_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value chars count is {}, must be greater than or equal to {}", - case_length, validator_length - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with CharsMinLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with CharsMinLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_chars_max_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputMaxLength { - #[graphql(validator(CharsMaxLength(length = "6")))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(CharsMaxLength(length = "6")))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = ["一2三4五", "壹2叁4伍6", "一2三4五6七", "壹2叁4伍6Ⅶ8"]; - - let validator_length = 6; - for case in &test_cases { - let field_query = format!("{{fieldParameter(id: \"{}\")}}", case); - let object_query = format!("{{inputObject(input: {{id: \"{}\"}})}}", case); - let case_length = case.chars().count(); - - if case_length > validator_length { - let should_fail_msg = format!( - "CharsMaxLength case {} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value chars count is {}, must be less than or equal to {}", - case_length, - validator_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value chars count is {}, must be less than or equal to {}", - case_length, - validator_length - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with CharsMaxLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with CharsMaxLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_string_email() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputEmail { - #[graphql(validator(Email))] - pub email: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter(&self, #[graphql(validator(Email))] _email: String) -> bool { - true - } - - async fn input_object(&self, _input: InputEmail) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - // Source https://gist.github.com/cjaoude/fd9910626629b53c4d25 - let test_cases = [ - // Invalid emails - ("plainaddress", true), - // ("#@%^%#$@#$@#.com", true), - ("@example.com", true), - ("Joe Smith ", true), - ("email.example.com", true), - // ("email@example@example.com", true), - // (".email@example.com", true), - // ("email.@example.com", true), - // ("email..email@example.com", true), - ("あいうえお@example.com", true), - // ("email@example.com (Joe Smith)", true), - // ("email@example", true), - // ("email@-example.com", true), - // ("email@example.web", true), - // ("email@111.222.333.44444", true), - // ("email@example..com", true), - // ("Abc..123@example.com", true), - // Valid Emails - ("email@example.com", false), - ("firstname.lastname@example.com", false), - ("email@subdomain.example.com", false), - ("firstname+lastname@example.com", false), - ("email@123.123.123.123", false), - ("email@[123.123.123.123]", false), - // This returns parsing error - // (r#""email"@example.com"#, false), - ("1234567890@example.com", false), - ("email@example-one.com", false), - ("_______@example.com", false), - ("email@example.name", false), - ("email@example.museum", false), - ("email@example.co.jp", false), - ("firstname-lastname@example.com", false), - ]; - - for (case, should_fail) in &test_cases { - let field_query = format!("{{fieldParameter(email: \"{}\")}}", case); - let object_query = format!("{{inputObject(input: {{email: \"{}\"}})}}", case); - - if *should_fail { - let should_fail_msg = format!( - "Email validation case {} should have failed, but did not", - case - ); - let field_error_msg = - "Invalid value for argument \"email\", invalid email format".to_owned(); - let object_error_msg = - "Invalid value for argument \"input.email\", invalid email format".to_owned(); - - // Testing FieldValidator - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - // Testing ObjectValidator - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - - // Field Paramter - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with Email", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with Email", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_string_mac() { - struct QueryRootWithColon; - struct QueryRootWithoutColon; - - #[derive(InputObject)] - struct InputMACWithColon { - #[graphql(validator(MAC(colon = "true")))] - pub mac: String, - } - - #[derive(InputObject)] - struct InputMACWithoutColon { - #[graphql(validator(MAC(colon = "false")))] - pub mac: String, - } - - #[Object] - impl QueryRootWithColon { - async fn field_parameter( - &self, - #[graphql(validator(MAC(colon = "true")))] _mac: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMACWithColon) -> bool { - true - } - } - - #[Object] - impl QueryRootWithoutColon { - async fn field_parameter( - &self, - #[graphql(validator(MAC(colon = "false")))] _mac: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMACWithoutColon) -> bool { - true - } - } - - let schema_with_colon = Schema::new(QueryRootWithColon, EmptyMutation, EmptySubscription); - let schema_without_colon = Schema::new(QueryRootWithoutColon, EmptyMutation, EmptySubscription); - - let valid_macs = vec![ - "28:11:32:7F:82:55", - "B3:DC:09:DE:6B:77", - "BD:FB:D5:F2:4B:1F", - "1E:5B:76:FF:23:04", - "00:00:00:00:00:00", - "2811327F8255", - "B3DC09DE6B77", - "BDFBD5F24B1F", - "1E5B76FF2304", - "000000000000", - ]; - - let invalid_macs = vec![ - "AB:CD", - "ABCD", - "ABCDEFHGIJKL", - "HJ11327F8255", - "ZZ:ZZ:ZZ:ZZ:ZZ:ZZ", - "AB:CD:EF:GH:IJ:KL", - "AB:CD:EF:HG:IJ:KL", - "HJ:11:32:7F:82:55", - ]; - - for mac in invalid_macs { - let field_query = format!("{{fieldParameter(mac: \"{}\")}}", mac); - let object_query = format!("{{inputObject(input: {{mac: \"{}\"}})}}", mac); - let should_fail_msg = format!( - "MAC validation case {} should have failed, but did not", - mac - ); - let field_error_msg = "Invalid value for argument \"mac\", invalid MAC format".to_owned(); - let object_error_msg = - "Invalid value for argument \"input.mac\", invalid MAC format".to_owned(); - - assert_eq!( - schema_without_colon - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg.clone(), - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - // Testing ObjectValidator - assert_eq!( - schema_without_colon - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg.clone(), - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema_with_colon - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - // Testing ObjectValidator - assert_eq!( - schema_with_colon - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } - - for mac in valid_macs { - let field_query = format!("{{fieldParameter(mac: \"{}\")}}", mac); - let object_query = format!("{{inputObject(input: {{mac: \"{}\"}})}}", mac); - let contains_colon = mac.contains(':'); - let should_fail_msg = format!( - "MAC validation case {} should have failed, but did not", - mac - ); - let field_error_msg = "Invalid value for argument \"mac\", invalid MAC format".to_owned(); - let object_error_msg = - "Invalid value for argument \"input.mac\", invalid MAC format".to_owned(); - let error_msg = format!("Schema returned error with test_string = {}", mac); - - if contains_colon { - // Field Paramter - assert_eq!( - schema_with_colon - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with MAC", - mac - ); - - assert_eq!( - schema_with_colon - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with MAC", - mac - ); - - assert_eq!( - schema_without_colon - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - // Testing ObjectValidator - assert_eq!( - schema_without_colon - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - assert_eq!( - schema_without_colon - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with MAC", - mac - ); - - assert_eq!( - schema_without_colon - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with MAC", - mac - ); - - assert_eq!( - schema_with_colon - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - // Testing ObjectValidator - assert_eq!( - schema_with_colon - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_int_range() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputIntRange { - #[graphql(validator(IntRange(min = "-2", max = "5")))] - pub id: i32, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(IntRange(min = "-2", max = "5")))] _id: i32, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputIntRange) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let min: i32 = -2; - let max: i32 = 5; - - for case in -10..10 { - let field_query = format!("{{fieldParameter(id: {})}}", case); - let object_query = format!("{{inputObject(input: {{id: {}}})}}", case); - - if case < min || case > max { - let should_fail_msg = format!("IntRange case {} should have failed, but did not", case); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value is {}, must be between {} and {}", - case, min, max - ); - let object_error_msg = format!("Invalid value for argument \"input.id\", the value is {}, must be between {} and {}", case, min, max); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with IntRange", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with IntRange", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_int_less_than() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputIntLessThan { - #[graphql(validator(IntLessThan(value = "5")))] - pub id: i32, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(IntLessThan(value = "5")))] _id: i32, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputIntLessThan) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let max: i32 = 5; - - for case in -10..10 { - let field_query = format!("{{fieldParameter(id: {})}}", case); - let object_query = format!("{{inputObject(input: {{id: {}}})}}", case); - - if case >= max { - let should_fail_msg = - format!("IntLessThan case {} should have failed, but did not", case); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value is {}, must be less than {}", - case, max - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value is {}, must be less than {}", - case, max - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with IntLessThan", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with IntLessThan", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_int_greater_than() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputIntGreaterThan { - #[graphql(validator(IntGreaterThan(value = "3")))] - pub id: i32, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(IntGreaterThan(value = "3")))] _id: i32, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputIntGreaterThan) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let min: i32 = 3; - - for case in -10..10 { - let field_query = format!("{{fieldParameter(id: {})}}", case); - let object_query = format!("{{inputObject(input: {{id: {}}})}}", case); - - if case <= min { - let should_fail_msg = format!( - "IntGreaterThan case {} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value is {}, must be greater than {}", - case, min - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value is {}, must be greater than {}", - case, min - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with IntGreaterThan", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with IntGreaterThan", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_int_nonzero() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputIntNonZero { - #[graphql(validator(IntNonZero))] - pub id: i32, - } - - #[Object] - impl QueryRoot { - async fn field_parameter(&self, #[graphql(validator(IntNonZero))] _id: i32) -> bool { - true - } - - async fn input_object(&self, _input: InputIntNonZero) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - for case in -10..10 { - let field_query = format!("{{fieldParameter(id: {})}}", case); - let object_query = format!("{{inputObject(input: {{id: {}}})}}", case); - if case == 0 { - let should_fail_msg = - format!("IntNonZero case {} should have failed, but did not", case); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value is {}, must be nonzero", - case - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value is {}, must be nonzero", - case - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with IntNonZero", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with IntNonZero", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_int_equal() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputIntEqual { - #[graphql(validator(IntEqual(value = "5")))] - pub id: i32, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(IntEqual(value = "5")))] _id: i32, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputIntEqual) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let equal_to = 5; - - for case in -10i32..10 { - let field_query = format!("{{fieldParameter(id: {})}}", case); - let object_query = format!("{{inputObject(input: {{id: {}}})}}", case); - if case != equal_to { - let should_fail_msg = - format!("IntNonZero case {} should have failed, but did not", case); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value is {}, must be equal to {}", - case, equal_to - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value is {}, must be equal to {}", - case, equal_to - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with IntEqual", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with IntEqual", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_list_max_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputListMaxLength { - #[graphql(validator(ListMaxLength(length = "5")))] - pub id: Vec, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(ListMaxLength(length = "5")))] _id: Vec, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputListMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let max_length: usize = 5; - let test_cases: Vec> = vec![ - vec![1], - vec![1, 2, 3], - vec![1, 2, 3, 4], - vec![1, 2, 3, 4, 5], - vec![1, 2, 3, 4, 5, 6], - vec![1, 2, 3, 4, 5, 6, 7, 8], - ]; - - for case in test_cases.iter() { - let field_query = format!("{{fieldParameter(id: {:?})}}", case); - let object_query = format!("{{inputObject(input: {{id: {:?}}})}}", case); - let case_length = case.len(); - if case_length > max_length { - let should_fail_msg = format!( - "ListMaxLength case {:?} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value length is {}, must be less than or equal to {}", - case_length, - max_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value length is {}, must be less than or equal to {}", - case_length, max_length - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {:?}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {:?} with ListMaxLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {:?} with ListMaxLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_list_min_length() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputListMinLength { - #[graphql(validator(ListMinLength(length = "4")))] - pub id: Vec, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(ListMinLength(length = "4")))] _id: Vec, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputListMinLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let min_length: usize = 4; - let test_cases: Vec> = vec![ - vec![1], - vec![1, 2, 3], - vec![1, 2, 3, 4], - vec![1, 2, 3, 4, 5], - vec![1, 2, 3, 4, 5, 6], - vec![1, 2, 3, 4, 5, 6, 7, 8], - ]; - - for case in test_cases.iter() { - let field_query = format!("{{fieldParameter(id: {:?})}}", case); - let object_query = format!("{{inputObject(input: {{id: {:?}}})}}", case); - let case_length = case.len(); - if case_length < min_length { - let should_fail_msg = format!( - "ListMinLength case {:?} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value length is {}, must be greater than or equal to {}", - case_length, - min_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value length is {}, must be greater than or equal to {}", - case_length, min_length - ); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {:?}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {:?} with ListMinLength", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {:?} with ListMinLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_operator_or() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputOrValidator { - #[graphql(validator(or(Email, MAC(colon = "false"))))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(or(Email, MAC(colon = "false"))))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputOrValidator) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = [ - ("2811327F8255", false), - ("B3DC09DE6B77", false), - ("BDFBD5F24B1F", false), - ("1E5B76FF2304", false), - ("000000000000", false), - ("email@example.com", false), - ("firstname.lastname@example.com", false), - ("email@subdomain.example.com", false), - ("firstname+lastname@example.com", false), - ("AB:CD", true), - ("ABCD", true), - ("ABCDEFHGIJKL", true), - ("HJ11327F8255", true), - ("ZZ:ZZ:ZZ:ZZ:ZZ:ZZ", true), - ("AB:CD:EF:GH:IJ:KL", true), - ("AB:CD:EF:HG:IJ:KL", true), - ("HJ:11:32:7F:82:55", true), - ("plainaddress", true), - ("@example.com", true), - ("Joe Smith ", true), - ("email.example.com", true), - ]; - - for (case, should_fail) in &test_cases { - let field_query = format!("{{fieldParameter(id: {:?})}}", case); - let object_query = format!("{{inputObject(input: {{id: {:?}}})}}", case); - if *should_fail { - let should_fail_msg = format!( - "OR operator case {:?} should have failed, but did not", - case - ); - - let field_error_msg = - "Invalid value for argument \"id\", invalid MAC format".to_owned(); - let object_error_msg = - "Invalid value for argument \"input.id\", invalid MAC format".to_owned(); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {:?}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {:?} with OR operator", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {:?} with OR operator", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_operator_and() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputAndValidator { - #[graphql(validator(and(Email, StringMinLength(length = "14"))))] - pub email: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(and(Email, StringMinLength(length = "14"))))] _email: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputAndValidator) -> bool { - true - } - } - - let min_length = 14; - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = [ - ("2811327F8255", true, true), - ("a@example.com", true, false), - ("firstname.lastname@example.com", false, false), - ("email@subdomain.example.com", false, false), - ]; - - for (case, should_fail, should_be_invalid_email) in &test_cases { - let case_length = case.len(); - let field_query = format!("{{fieldParameter(email: {:?})}}", case); - let object_query = format!("{{inputObject(input: {{email: {:?}}})}}", case); - if *should_fail { - let should_fail_msg = format!( - "AND operator case {:?} should have failed, but did not", - case - ); - - let field_error_msg = if *should_be_invalid_email { - "Invalid value for argument \"email\", invalid email format".to_owned() - } else { - format!("Invalid value for argument \"email\", the value length is {}, must be greater than or equal to {}", case_length, min_length) - }; - - let object_error_msg = if *should_be_invalid_email { - "Invalid value for argument \"input.email\", invalid email format".to_owned() - } else { - format!("Invalid value for argument \"input.email\", the value length is {}, must be greater than or equal to {}", case_length, min_length) - }; - - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 14 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test case = {:?}", case); - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {:?} with AND operator", - case - ); - - assert_eq!( - schema - .execute(&object_query) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {:?} with AND operator", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_input_validator_variable() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputMaxLength { - #[graphql(validator(StringMinLength(length = "6")))] - pub id: String, - } - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(StringMinLength(length = "6")))] _id: String, - ) -> bool { - true - } - - async fn input_object(&self, _input: InputMaxLength) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - let test_cases = [ - "abc", - "acbce", - "abcdef", - "abcdefghi", - "abcdefghijkl", - "abcdefghijklmnop", - ]; - - let validator_length = 6; - for case in &test_cases { - let mut variables = Variables::default(); - variables.insert(Name::new("id"), Value::String(case.to_string())); - - let field_query = "query($id: String!) {fieldParameter(id: $id)}"; - let object_query = "query($id: String!) {inputObject(input: {id: $id})}"; - let case_length = case.len(); - - if case_length < validator_length { - let should_fail_msg = format!( - "StringMinValue case {} should have failed, but did not", - case - ); - - let field_error_msg = format!( - "Invalid value for argument \"id\", the value length is {}, must be greater than or equal to {}", - case_length, validator_length - ); - let object_error_msg = format!( - "Invalid value for argument \"input.id\", the value length is {}, must be greater than or equal to {}", - case_length, validator_length - ); - - assert_eq!( - schema - .execute(Request::new(field_query).variables(variables.clone())) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: field_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 37 - }), - path: Vec::new(), - extensions: None, - }] - ); - - assert_eq!( - schema - .execute(Request::new(object_query).variables(variables.clone())) - .await - .into_result() - .expect_err(&should_fail_msg[..]), - vec![ServerError { - message: object_error_msg, - source: None, - locations: vec!(Pos { - line: 1, - column: 34 - }), - path: Vec::new(), - extensions: None, - }] - ); - } else { - let error_msg = format!("Schema returned error with test_string = {}", case); - assert_eq!( - schema - .execute(Request::new(field_query).variables(variables.clone())) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"fieldParameter": true}), - "Failed to validate {} with StringMinLength", - case - ); - - assert_eq!( - schema - .execute(Request::new(object_query).variables(variables.clone())) - .await - .into_result() - .expect(&error_msg[..]) - .data, - value!({"inputObject": true}), - "Failed to validate {} with StringMinLength", - case - ); - } - } -} - -#[tokio::test] -pub async fn test_custom_input_validator_with_extensions() { - pub struct IntGreaterThanZero; - - impl InputValueValidator for IntGreaterThanZero { - fn is_valid_with_extensions(&self, value: &Value) -> Result<(), Error> { - if let Value::Number(n) = value { - if let Some(n) = n.as_i64() { - if n <= 0 { - let e = Error::new("Value must be greater than 0") - .extend_with(|_, e| e.set("code", 400)); - return Err(e); - } - } - } - Ok(()) - } - } - - struct QueryRoot; - - #[Object] - impl QueryRoot { - async fn field_parameter( - &self, - #[graphql(validator(IntGreaterThanZero))] _id: i32, - ) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - let case = 0; - let field_query = format!("{{fieldParameter(id: {})}}", case); - let should_fail_msg = "IntGreaterThanZero must failed, but not"; - let field_error_msg = "Invalid value for argument \"id\", Value must be greater than 0"; - - let mut error_extensions = ErrorExtensionValues::default(); - error_extensions.set("code", 400); - - assert_eq!( - schema - .execute(&field_query) - .await - .into_result() - .expect_err(should_fail_msg), - vec![ServerError { - message: field_error_msg.into(), - source: None, - locations: vec!(Pos { - line: 1, - column: 17 - }), - path: Vec::new(), - extensions: Some(error_extensions), - }] - ); -} - -#[tokio::test] -pub async fn test_input_validator_list() { - struct QueryRoot; - - #[derive(InputObject)] - struct InputEmail { - #[graphql(validator(list(Email)))] - pub emails: Vec, - } - - #[Object] - impl QueryRoot { - async fn value(&self, #[graphql(validator(list(Email)))] _emails: Vec) -> bool { - true - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - assert_eq!( - schema - .execute( - r#" - { - value( - emails: [ - "a@a.com", - "b@abc.com", - ] - ) - }"# - ) - .await - .into_result() - .unwrap() - .data, - value!({"value": true}) - ); - - assert_eq!( - schema - .execute( - r#" - { - value( - emails: [ - "123456", - ] - ) - }"# - ) - .await - .into_result() - .unwrap_err()[0] - .message, - "Invalid value for argument \"emails\", invalid email format" - ); -}