diff --git a/Cargo.lock b/Cargo.lock index 559e0ae..f00a438 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,6 +365,8 @@ name = "fortifier-example-generics" version = "0.2.4" dependencies = [ "fortifier", + "serde", + "uuid", ] [[package]] diff --git a/examples/generics/Cargo.toml b/examples/generics/Cargo.toml index 2cf87ad..f119de5 100644 --- a/examples/generics/Cargo.toml +++ b/examples/generics/Cargo.toml @@ -10,7 +10,9 @@ repository.workspace = true version.workspace = true [dependencies] -fortifier.workspace = true +fortifier = { workspace = true, features = ["email-address", "serde"] } +serde = { workspace = true, features = ["derive"] } +uuid = { workspace = true, features = ["serde"] } [lints] workspace = true diff --git a/examples/generics/src/action.rs b/examples/generics/src/action.rs new file mode 100644 index 0000000..a1c0cb5 --- /dev/null +++ b/examples/generics/src/action.rs @@ -0,0 +1,38 @@ +use std::fmt::Debug; + +use fortifier::{Validate, ValidateWithContext}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +#[serde(untagged, rename_all = "camelCase")] +pub enum CreateOrUpdate +where + C: Debug + Send + Sync + ValidateWithContext, + U: Debug + Send + Sync + ValidateWithContext, +{ + Update { + #[validate(skip)] + id: Uuid, + #[serde(flatten)] + data: U, + }, + Create { + #[serde(flatten)] + data: C, + }, +} + +pub type CreateOrUpdateUser = CreateOrUpdate; + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +pub struct CreateUser { + #[validate(email_address)] + pub email: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +pub struct UpdateUser { + #[validate(email_address)] + pub email: Option, +} diff --git a/examples/generics/src/bounds.rs b/examples/generics/src/bounds.rs new file mode 100644 index 0000000..d30603e --- /dev/null +++ b/examples/generics/src/bounds.rs @@ -0,0 +1,40 @@ +use std::fmt::Debug; + +use fortifier::Validate; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Validate)] +#[validate(custom(function = validate_min_max, error = BoundsMinMaxError))] +pub struct Bounds +where + T: Clone + Debug + PartialEq + PartialOrd + Send + Sync, +{ + pub min: Option, + pub max: Option, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct BoundsMinMaxError +where + T: Debug + PartialEq, +{ + pub min: T, + pub max: T, +} + +fn validate_min_max(value: &Bounds) -> Result<(), BoundsMinMaxError> +where + T: Clone + Debug + PartialEq + PartialOrd + Send + Sync, +{ + if let Some(min) = &value.min + && let Some(max) = &value.max + && min > max + { + Err(BoundsMinMaxError { + min: min.clone(), + max: max.clone(), + }) + } else { + Ok(()) + } +} diff --git a/examples/generics/src/main.rs b/examples/generics/src/main.rs index 0f7ae30..d1cbbf3 100644 --- a/examples/generics/src/main.rs +++ b/examples/generics/src/main.rs @@ -1,44 +1,32 @@ -use std::fmt::Debug; +mod action; +mod bounds; use fortifier::{Validate, ValidationErrors}; +use uuid::Uuid; -#[derive(Validate)] -#[validate(custom(function = validate_min_max, error = BoundsMinMaxError))] -struct Bounds -where - T: Clone + Debug + PartialEq + PartialOrd, -{ - min: Option, - max: Option, -} +use crate::{ + action::{CreateOrUpdateUser, CreateUser, UpdateUser}, + bounds::{Bounds, BoundsMinMaxError, BoundsValidationError}, +}; -#[derive(Debug, PartialEq)] -struct BoundsMinMaxError -where - T: Debug + PartialEq, -{ - min: T, - max: T, -} +fn main() { + let data = CreateOrUpdateUser::Create { + data: CreateUser { + email: "amy.pond@example.com".to_owned(), + }, + }; -fn validate_min_max(value: &Bounds) -> Result<(), BoundsMinMaxError> -where - T: Clone + Debug + PartialEq + PartialOrd, -{ - if let Some(min) = &value.min - && let Some(max) = &value.max - && min > max - { - Err(BoundsMinMaxError { - min: min.clone(), - max: max.clone(), - }) - } else { - Ok(()) - } -} + assert!(data.validate_sync().is_ok()); + + let data = CreateOrUpdateUser::Update { + id: Uuid::nil(), + data: UpdateUser { + email: Some("amy.pond@example.com".to_owned()), + }, + }; + + assert!(data.validate_sync().is_ok()); -fn main() { let bounds = Bounds { min: Some(1), max: Some(10), diff --git a/packages/fortifier-macros-tests/tests/validate/generics_type_pass.rs b/packages/fortifier-macros-tests/tests/validate/generics_type_pass.rs new file mode 100644 index 0000000..9ace5fd --- /dev/null +++ b/packages/fortifier-macros-tests/tests/validate/generics_type_pass.rs @@ -0,0 +1,57 @@ +use std::fmt::Debug; + +use fortifier::{Validate, ValidateWithContext}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +#[serde(untagged, rename_all = "camelCase")] +enum CreateOrUpdate +where + C: Debug + Send + Sync + ValidateWithContext, + U: Debug + Send + Sync + ValidateWithContext, +{ + Update { + #[validate(skip)] + id: Uuid, + #[serde(flatten)] + data: U, + }, + Create { + #[serde(flatten)] + data: C, + }, +} + +type CreateOrUpdateUser = CreateOrUpdate; + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +struct CreateUser { + #[validate(email_address)] + email: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Validate)] +struct UpdateUser { + #[validate(email_address)] + email: Option, +} + +fn main() { + let data = CreateOrUpdateUser::Create { + data: CreateUser { + email: "amy.pond@example.com".to_owned(), + }, + }; + + assert!(data.validate_sync().is_ok()); + + let data = CreateOrUpdateUser::Update { + id: Uuid::nil(), + data: UpdateUser { + email: Some("amy.pond@example.com".to_owned()), + }, + }; + + assert!(data.validate_sync().is_ok()); +} diff --git a/packages/fortifier-macros-tests/tests/validations/custom/root_generics_pass.rs b/packages/fortifier-macros-tests/tests/validations/custom/root_generics_pass.rs index 9d9a6d1..0b33d4f 100644 --- a/packages/fortifier-macros-tests/tests/validations/custom/root_generics_pass.rs +++ b/packages/fortifier-macros-tests/tests/validations/custom/root_generics_pass.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; #[validate(custom(function = validate_min_max, error = BoundsMinMaxError))] struct Bounds where - T: Clone + Debug + PartialEq + PartialOrd, + T: Clone + Debug + PartialEq + PartialOrd + Send + Sync, { min: Option, max: Option, @@ -16,7 +16,7 @@ where #[derive(Debug, Deserialize, PartialEq, Serialize)] struct BoundsMinMaxError where - T: Debug + PartialEq, + T: Debug + PartialEq + Send + Sync, { min: T, max: T, @@ -24,7 +24,7 @@ where fn validate_min_max(value: &Bounds) -> Result<(), BoundsMinMaxError> where - T: Clone + Debug + PartialEq + PartialOrd, + T: Clone + Debug + PartialEq + PartialOrd + Send + Sync, { if let Some(min) = &value.min && let Some(max) = &value.max diff --git a/packages/fortifier-macros/src/validate/generics.rs b/packages/fortifier-macros/src/generics.rs similarity index 61% rename from packages/fortifier-macros/src/validate/generics.rs rename to packages/fortifier-macros/src/generics.rs index ba3aba1..085d42f 100644 --- a/packages/fortifier-macros/src/validate/generics.rs +++ b/packages/fortifier-macros/src/generics.rs @@ -1,26 +1,64 @@ use syn::{ ConstParam, GenericArgument, GenericParam, Generics, Ident, Lifetime, Path, PathArguments, - Type, TypeParam, WhereClause, WherePredicate, punctuated::Punctuated, + Type, TypeParam, TypePath, WhereClause, WherePredicate, punctuated::Punctuated, }; -pub fn filter_generics_by_generic_arguments( - generics: &Generics, - arguments: &[GenericArgument], -) -> Generics { +#[derive(Clone)] +pub enum Generic { + Argument(GenericArgument), + Param(GenericParam), +} + +pub fn generic_arguments(r#type: &TypePath) -> Vec { + if let Some(segment) = r#type.path.segments.last() + && let PathArguments::AngleBracketed(arguments) = &segment.arguments + { + arguments + .args + .iter() + .cloned() + .map(Generic::Argument) + .collect() + } else { + vec![] + } +} + +pub fn filter_generics(generics: &Generics, arguments_or_params: &[Generic]) -> Generics { + let generic_arguments = arguments_or_params.iter().filter_map(|generic| { + if let Generic::Argument(argument) = generic { + Some(argument) + } else { + None + } + }); + let generic_params = arguments_or_params.iter().filter_map(|generic| { + if let Generic::Param(param) = generic { + Some(param) + } else { + None + } + }); + let params = Punctuated::from_iter( generics .params .iter() - .filter(|param| match param { - GenericParam::Lifetime(param) => arguments - .iter() - .any(|argument| lifetime_matches_argument(¶m.lifetime, argument)), - GenericParam::Type(param) => arguments - .iter() - .any(|argument| type_param_matches_argument(param, argument)), - GenericParam::Const(param) => arguments - .iter() - .any(|argument| const_param_matches_argument(param, argument)), + .filter(|param| { + generic_params + .clone() + .any(|generic_param| generic_param_equals_generic_param(generic_param, param)) + || match param { + GenericParam::Lifetime(param) => generic_arguments + .clone() + .any(|argument| lifetime_matches_argument(¶m.lifetime, argument)), + GenericParam::Type(param) => generic_arguments + .clone() + .any(|argument| type_param_matches_argument(param, argument)), + GenericParam::Const(param) => generic_arguments + .clone() + .any(|argument| const_param_matches_argument(param, argument)), + } }) .cloned(), ); @@ -35,12 +73,22 @@ pub fn filter_generics_by_generic_arguments( .predicates .iter() .filter(|predicate| match predicate { - WherePredicate::Lifetime(predicate) => arguments.iter().any(|argument| { - lifetime_matches_argument(&predicate.lifetime, argument) - }), - WherePredicate::Type(predicate) => arguments - .iter() - .any(|argument| type_matches_argument(&predicate.bounded_ty, argument)), + WherePredicate::Lifetime(predicate) => { + generic_params + .clone() + .any(|param| lifetime_matches_param(&predicate.lifetime, param)) + || generic_arguments.clone().any(|argument| { + lifetime_matches_argument(&predicate.lifetime, argument) + }) + } + WherePredicate::Type(predicate) => { + generic_params + .clone() + .any(|param| type_matches_param(&predicate.bounded_ty, param)) + || generic_arguments.clone().any(|argument| { + type_matches_argument(&predicate.bounded_ty, argument) + }) + } _ => false, }) .cloned(), @@ -59,6 +107,10 @@ fn lifetime_matches_argument(lifetime: &Lifetime, argument: &GenericArgument) -> matches!(argument, GenericArgument::Lifetime(argument_lifetime) if argument_lifetime.ident == lifetime.ident) } +fn lifetime_matches_param(lifetime: &Lifetime, param: &GenericParam) -> bool { + matches!(param, GenericParam::Lifetime(param) if param.lifetime.ident == lifetime.ident) +} + fn type_matches_argument(r#type: &Type, argument: &GenericArgument) -> bool { match argument { GenericArgument::Lifetime(_) => false, @@ -77,6 +129,14 @@ fn type_matches_argument(r#type: &Type, argument: &GenericArgument) -> bool { } } +fn type_matches_param(r#type: &Type, param: &GenericParam) -> bool { + match param { + GenericParam::Lifetime(_) => false, + GenericParam::Type(param_type) => type_matches_ident(r#type, ¶m_type.ident), + GenericParam::Const(_expr) => false, + } +} + fn type_param_matches_argument(param: &TypeParam, argument: &GenericArgument) -> bool { match argument { GenericArgument::Lifetime(_) => false, @@ -175,3 +235,14 @@ fn path_argument_equals_path_argument(a: &PathArguments, b: &PathArguments) -> b _ => false, } } + +fn generic_param_equals_generic_param(a: &GenericParam, b: &GenericParam) -> bool { + match (a, b) { + (GenericParam::Lifetime(a), GenericParam::Lifetime(b)) => { + a.lifetime.ident == b.lifetime.ident + } + (GenericParam::Type(a), GenericParam::Type(b)) => a.ident == b.ident, + (GenericParam::Const(a), GenericParam::Const(b)) => a.ident == b.ident, + _ => false, + } +} diff --git a/packages/fortifier-macros/src/attributes.rs b/packages/fortifier-macros/src/integrations.rs similarity index 70% rename from packages/fortifier-macros/src/attributes.rs rename to packages/fortifier-macros/src/integrations.rs index d606777..55aded7 100644 --- a/packages/fortifier-macros/src/attributes.rs +++ b/packages/fortifier-macros/src/integrations.rs @@ -56,3 +56,24 @@ pub fn enum_field_attributes() -> TokenStream { #( #attributes )* } } + +pub fn where_predicate(error_type: TokenStream) -> TokenStream { + #[allow(unused_mut)] + let mut lifetimes = TokenStream::new(); + #[allow(unused_mut)] + let mut traits = TokenStream::new(); + + #[cfg(feature = "serde")] + { + use proc_macro_crate::crate_name; + + if crate_name("serde").is_ok() { + lifetimes = quote!(for<'fde>); + traits = quote!(+ ::serde::Deserialize<'fde> + ::serde::Serialize); + } + } + + quote! { + #lifetimes #error_type: Debug + PartialEq #traits + } +} diff --git a/packages/fortifier-macros/src/lib.rs b/packages/fortifier-macros/src/lib.rs index 7e542ee..5d3af06 100644 --- a/packages/fortifier-macros/src/lib.rs +++ b/packages/fortifier-macros/src/lib.rs @@ -2,7 +2,8 @@ //! Fortifier macros. -mod attributes; +mod generics; +mod integrations; mod util; mod validate; mod validation; diff --git a/packages/fortifier-macros/src/util.rs b/packages/fortifier-macros/src/util.rs index a50cc9f..01d4d79 100644 --- a/packages/fortifier-macros/src/util.rs +++ b/packages/fortifier-macros/src/util.rs @@ -1,6 +1,6 @@ use convert_case::{Case, Casing}; use quote::format_ident; -use syn::{GenericArgument, Ident, PathArguments, TypePath}; +use syn::Ident; pub fn upper_camel_ident(ident: &Ident) -> Ident { let s = ident.to_string(); @@ -11,13 +11,3 @@ pub fn upper_camel_ident(ident: &Ident) -> Ident { format_ident!("{}", s.to_case(Case::UpperCamel)) } } - -pub fn generic_arguments(r#type: &TypePath) -> Vec { - if let Some(segment) = r#type.path.segments.last() - && let PathArguments::AngleBracketed(arguments) = &segment.arguments - { - arguments.args.iter().cloned().collect() - } else { - vec![] - } -} diff --git a/packages/fortifier-macros/src/validate.rs b/packages/fortifier-macros/src/validate.rs index 2239ac5..0bcbaa7 100644 --- a/packages/fortifier-macros/src/validate.rs +++ b/packages/fortifier-macros/src/validate.rs @@ -3,7 +3,6 @@ mod r#enum; mod error; mod field; mod fields; -mod generics; mod r#struct; mod r#type; mod r#union; @@ -110,23 +109,43 @@ impl<'a> ToTokens for Validate<'a> { }), }; - let (error_type, error_definition) = if let Some(ErrorType { - r#type, definition, .. + let (error_type, where_predicates, error_definition) = if let Some(ErrorType { + r#type, + where_predicates, + definition, + .. }) = self.error_type() { - (r#type, definition) + (r#type, where_predicates, definition) } else { let visibility = &self.visibility; let error_ident = format_error_ident(self.ident); ( error_ident.to_token_stream(), + vec![], Some(quote! { #visibility type #error_ident = ::std::convert::Infallible; }), ) }; + let where_clause = if let Some(where_clause) = where_clause { + if where_clause.predicates.trailing_punct() { + quote! { + #where_clause #( #where_predicates ),* + } + } else { + quote! { + #where_clause, #( #where_predicates ),* + } + } + } else { + quote! { + where #( #where_predicates ),* + } + }; + let sync_validations = self.validations(Execution::Sync); let async_validations = self.validations(Execution::Async); diff --git a/packages/fortifier-macros/src/validate/error.rs b/packages/fortifier-macros/src/validate/error.rs index 836f5c2..9919a4a 100644 --- a/packages/fortifier-macros/src/validate/error.rs +++ b/packages/fortifier-macros/src/validate/error.rs @@ -1,9 +1,10 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{GenericArgument, Generics, Ident, Visibility}; +use syn::{Generics, Ident, Visibility}; use crate::{ - attributes::enum_attributes, validate::generics::filter_generics_by_generic_arguments, + generics::{Generic, filter_generics}, + integrations::enum_attributes, validation::Validation, }; @@ -18,7 +19,8 @@ pub fn format_error_ident_with_prefix(prefix: &Ident, ident: &Ident) -> Ident { pub struct ErrorType { pub variant_ident: Ident, pub r#type: TokenStream, - pub generic_arguments: Vec, + pub generics: Vec, + pub where_predicates: Vec, pub definition: Option, } @@ -33,15 +35,20 @@ pub fn error_type( let ident = format_error_ident_with_prefix(prefix, error_ident); let variant_ident = validations.iter().map(|validation| validation.ident()); let variant_type = validations.iter().map(|validation| validation.error_type()); - let generic_arguments = validations + let generics = validations .iter() - .flat_map(|validation| validation.error_generic_arguments()) + .flat_map(|validation| validation.error_generics()) + .collect(); + let where_predicates = validations + .iter() + .flat_map(|validation| validation.error_where_predicates()) .collect(); Some(ErrorType { variant_ident: error_ident.clone(), r#type: ident.to_token_stream(), - generic_arguments, + generics, + where_predicates, definition: Some(quote! { #[derive(Debug, PartialEq)] #attributes @@ -54,7 +61,8 @@ pub fn error_type( validations.first().map(|validation| ErrorType { variant_ident: error_ident.clone(), r#type: validation.error_type(), - generic_arguments: validation.error_generic_arguments(), + generics: validation.error_generics(), + where_predicates: validation.error_where_predicates(), definition: None, }) } @@ -97,24 +105,52 @@ pub fn combined_error_type( .flat_map(|error_type| &error_type.definition), ); - let generic_arguments = root_error_type + let generic_arguments_or_params = root_error_type + .into_iter() + .flat_map(|error_type| &error_type.generics) + .chain( + error_types + .iter() + .flat_map(|error_type| &error_type.generics), + ) + .cloned() + .collect::>(); + + let where_predicates = root_error_type .into_iter() - .flat_map(|error_type| &error_type.generic_arguments) + .flat_map(|error_type| &error_type.where_predicates) .chain( error_types .iter() - .flat_map(|error_type| &error_type.generic_arguments), + .flat_map(|error_type| &error_type.where_predicates), ) .cloned() .collect::>(); - let generics = filter_generics_by_generic_arguments(generics, &generic_arguments); + let generics = filter_generics(generics, &generic_arguments_or_params); let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); + let where_clause = if let Some(where_clause) = where_clause { + if where_clause.predicates.trailing_punct() { + quote! { + #where_clause #( #where_predicates ),* + } + } else { + quote! { + #where_clause, #( #where_predicates ),* + } + } + } else { + quote! { + where #( #where_predicates ),* + } + }; + Some(ErrorType { variant_ident: variant_ident.clone(), r#type: quote!(#ident #type_generics), - generic_arguments, + generics: generic_arguments_or_params, + where_predicates, definition: Some(quote! { #[allow(dead_code)] #[derive(Debug, PartialEq)] diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index 1ee5ba2..c4ea49a 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -133,10 +133,12 @@ impl<'a> ValidateField<'a> { && result.validations.is_empty() && let Some(nested_type) = should_validate_type(generics, &field.ty) { - if let KnownOrUnknown::Known(nested_type) = nested_type { - result - .validations - .push(Box::new(Nested::new(syn::parse2(nested_type)?))); + if let KnownOrUnknown::Known(error_type) = nested_type.error_type { + result.validations.push(Box::new(Nested::new( + syn::parse2(error_type)?, + nested_type.generic_params, + nested_type.where_predicates, + ))); } else { return Err(Error::new_spanned( field, diff --git a/packages/fortifier-macros/src/validate/type.rs b/packages/fortifier-macros/src/validate/type.rs index 030dccb..7fe01ed 100644 --- a/packages/fortifier-macros/src/validate/type.rs +++ b/packages/fortifier-macros/src/validate/type.rs @@ -1,11 +1,11 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{ - GenericArgument, Generics, Path, PathArguments, PathSegment, Type, TypeParamBound, - WherePredicate, punctuated::Punctuated, token::PathSep, + GenericArgument, GenericParam, Generics, Path, PathArguments, PathSegment, Type, TypeParam, + TypeParamBound, WherePredicate, punctuated::Punctuated, token::PathSep, }; -use crate::validate::error::format_error_ident; +use crate::{integrations::where_predicate, validate::error::format_error_ident}; /// Primitive and built-in types. /// @@ -228,6 +228,22 @@ const ECOSYSTEM_TYPES: [&str; 65] = [ "uuid::Uuid", ]; +pub struct ValidateResult { + pub error_type: KnownOrUnknown, + pub generic_params: Vec, + pub where_predicates: Vec, +} + +impl ValidateResult { + fn unknown() -> Self { + Self { + error_type: KnownOrUnknown::Unknown, + generic_params: vec![], + where_predicates: vec![], + } + } +} + fn path_to_string(path: &Path) -> String { path.segments .iter() @@ -247,51 +263,71 @@ fn is_validate_path(path: &Path) -> bool { fn should_validate_generic_argument( generics: &Generics, arg: &GenericArgument, -) -> Option> { +) -> Option { match arg { - GenericArgument::Lifetime(_) => Some(KnownOrUnknown::Unknown), + GenericArgument::Lifetime(_) => Some(ValidateResult::unknown()), GenericArgument::Type(r#type) => should_validate_type(generics, r#type), // TODO: Const. - GenericArgument::Const(_expr) => Some(KnownOrUnknown::Unknown), + GenericArgument::Const(_expr) => Some(ValidateResult::unknown()), // TODO: Associated type. - GenericArgument::AssocType(_assoc_type) => Some(KnownOrUnknown::Unknown), + GenericArgument::AssocType(_assoc_type) => Some(ValidateResult::unknown()), // TODO: Associated const. - GenericArgument::AssocConst(_assoc_const) => Some(KnownOrUnknown::Unknown), + GenericArgument::AssocConst(_assoc_const) => Some(ValidateResult::unknown()), // TODO: Constraint. - GenericArgument::Constraint(_constraint) => Some(KnownOrUnknown::Unknown), - _ => Some(KnownOrUnknown::Unknown), + GenericArgument::Constraint(_constraint) => Some(ValidateResult::unknown()), + _ => Some(ValidateResult::unknown()), } } fn should_validate_type_param_bounds<'a>( mut bounds: impl Iterator, -) -> Option> { + param: Option<&TypeParam>, +) -> Option { bounds .any(|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path))) - .then_some(KnownOrUnknown::Unknown) + .then(|| { + if let Some(param) = param { + let ident = ¶m.ident; + + let error_type = quote!(<#ident as ::fortifier::ValidateWithContext>::Error); + + ValidateResult { + error_type: KnownOrUnknown::Known(error_type.clone()), + generic_params: vec![GenericParam::Type(param.clone())], + where_predicates: vec![where_predicate(error_type)], + } + } else { + ValidateResult::unknown() + } + }) } -fn should_validate_path(generics: &Generics, path: &Path) -> Option> { +fn should_validate_path(generics: &Generics, path: &Path) -> Option { if let Some(ident) = path.get_ident() { if PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str()) { return None; } if let Some(param) = generics.type_params().find(|param| param.ident == *ident) { - return should_validate_type_param_bounds(param.bounds.iter()).or_else(|| { - generics.where_clause.as_ref().and_then(|where_clause| { - where_clause.predicates.iter().find_map(|predicate| { - if let WherePredicate::Type(predicate) = predicate - && let Type::Path(predicate_type) = &predicate.bounded_ty - && predicate_type.path.is_ident(ident) - { - should_validate_type_param_bounds(predicate.bounds.iter()) - } else { - None - } + return should_validate_type_param_bounds(param.bounds.iter(), Some(param)).or_else( + || { + generics.where_clause.as_ref().and_then(|where_clause| { + where_clause.predicates.iter().find_map(|predicate| { + if let WherePredicate::Type(predicate) = predicate + && let Type::Path(predicate_type) = &predicate.bounded_ty + && predicate_type.path.is_ident(ident) + { + should_validate_type_param_bounds( + predicate.bounds.iter(), + Some(param), + ) + } else { + None + } + }) }) - }) - }); + }, + ); } } @@ -311,13 +347,15 @@ fn should_validate_path(generics: &Generics, path: &Path) -> Option { - KnownOrUnknown::Known(quote!(::fortifier::IndexedValidationError<#error_type>)) - } - KnownOrUnknown::Unknown => KnownOrUnknown::Unknown, + return should_validate_generic_argument(generics, argument).map(|mut result| match result + .error_type + { + KnownOrUnknown::Known(error_type) => { + result.error_type = + KnownOrUnknown::Known(quote!(::fortifier::IndexedValidationError<#error_type>)); + result } + KnownOrUnknown::Unknown => result, }); } @@ -353,7 +391,11 @@ fn should_validate_path(generics: &Generics, path: &Path) -> Option KnownOrUnknown { } } -pub fn should_validate_type( - generics: &Generics, - r#type: &Type, -) -> Option> { +pub fn should_validate_type(generics: &Generics, r#type: &Type) -> Option { match r#type { Type::Array(r#type) => { - should_validate_type(generics, &r#type.elem).map(|error_type| { - error_type.map(|error_type| quote!(::fortifier::IndexedValidationError<#error_type>)) + should_validate_type(generics, &r#type.elem).map(|mut result| { + result.error_type = result.error_type.map(|error_type| quote!(::fortifier::IndexedValidationError<#error_type>)); + result }) }, Type::BareFn(_) => None, @@ -390,23 +430,28 @@ pub fn should_validate_type( r#type.bounds .iter() .any(|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path))) - .then_some(KnownOrUnknown::Unknown) + .then_some(ValidateResult::unknown()) }, - Type::Infer(_) => Some(KnownOrUnknown::Unknown), - Type::Macro(_) => Some(KnownOrUnknown::Unknown), + Type::Infer(_) => Some(ValidateResult::unknown()), + Type::Macro(_) => Some(ValidateResult::unknown()), Type::Never(_) => None, Type::Paren(r#type) => should_validate_type(generics, &r#type.elem), Type::Path(r#type) => should_validate_path(generics, &r#type.path), Type::Ptr(r#type) => should_validate_type(generics, &r#type.elem), Type::Reference(r#type) => should_validate_type(generics,&r#type.elem), Type::Slice(r#type) => { - should_validate_type(generics, &r#type.elem).map(|error_type| { - error_type.map(|error_type| quote!(::fortifier::IndexedValidationError<#error_type>)) + should_validate_type(generics, &r#type.elem).map(|mut result| { + result.error_type = result.error_type.map(|error_type| quote!(::fortifier::IndexedValidationError<#error_type>)); + result }) }, - Type::TraitObject(r#type) => should_validate_type_param_bounds(r#type.bounds.iter()), + Type::TraitObject(r#type) => should_validate_type_param_bounds( r#type.bounds.iter(), None), Type::Tuple(r#type) => { - (!r#type.elems.is_empty() && r#type.elems.iter().all(|r#type| should_validate_type(generics, r#type).is_some())).then_some(KnownOrUnknown::Unknown) + (!r#type.elems.is_empty() && + r#type.elems + .iter() + .all(|r#type| should_validate_type(generics, r#type).is_some())) + .then_some(ValidateResult::unknown()) } Type::Verbatim(_) => None, _ => None, @@ -415,6 +460,7 @@ pub fn should_validate_type( #[cfg(test)] mod tests { + use proc_macro2::TokenStream; use quote::quote; use syn::{GenericParam, Generics, punctuated::Punctuated}; @@ -431,8 +477,21 @@ mod tests { tokens: TokenStream, generics: Generics, ) -> Option> { - should_validate_type(&generics, &syn::parse2(tokens).expect("valid type")) - .map(|value| value.map(|value| value.to_string().replace(' ', ""))) + should_validate_type(&generics, &syn::parse2(tokens).expect("valid type")).map(|result| { + result.error_type.map(|error_type| { + error_type + .to_string() + .replace(":: ", "::") + .replace(" ::", "::") + .replace(" as::", " as ::") + .replace("< ", "<") + .replace(" <", "<") + .replace("> ", ">") + .replace(" >", ">") + .trim() + .to_string() + }) + }) } #[test] @@ -668,8 +727,6 @@ mod tests { #[test] fn should_validate_with_generics() { - // TODO: Output error type as `::Error` if possible. - assert_eq!( validate_with_generics( quote!(T), @@ -683,7 +740,9 @@ mod tests { where_clause: None } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -698,7 +757,9 @@ mod tests { where_clause: None } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::fortifier::IndexedValidationError<::Error>".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -713,7 +774,9 @@ mod tests { where_clause: None } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -728,7 +791,9 @@ mod tests { where_clause: None } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); assert_eq!( @@ -745,7 +810,9 @@ mod tests { ) } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -761,7 +828,9 @@ mod tests { ) } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::fortifier::IndexedValidationError<::Error>".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -778,7 +847,9 @@ mod tests { ) } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); assert_eq!( validate_with_generics( @@ -795,7 +866,9 @@ mod tests { ) } ), - Some(KnownOrUnknown::Unknown) + Some(KnownOrUnknown::Known( + "::Error".to_owned() + )) ); } diff --git a/packages/fortifier-macros/src/validation.rs b/packages/fortifier-macros/src/validation.rs index 6495d99..544093a 100644 --- a/packages/fortifier-macros/src/validation.rs +++ b/packages/fortifier-macros/src/validation.rs @@ -1,5 +1,7 @@ use proc_macro2::TokenStream; -use syn::{GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Ident, Result, Type, meta::ParseNestedMeta}; + +use crate::generics::Generic; #[derive(Clone, Copy)] pub enum Execution { @@ -16,7 +18,9 @@ pub trait Validation { fn error_type(&self) -> TokenStream; - fn error_generic_arguments(&self) -> Vec; + fn error_generics(&self) -> Vec; + + fn error_where_predicates(&self) -> Vec; fn expr(&self, execution: Execution, expr: &TokenStream) -> Option; } diff --git a/packages/fortifier-macros/src/validations/custom.rs b/packages/fortifier-macros/src/validations/custom.rs index 8aa2a66..2e7d1e1 100644 --- a/packages/fortifier-macros/src/validations/custom.rs +++ b/packages/fortifier-macros/src/validations/custom.rs @@ -1,9 +1,10 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{GenericArgument, Ident, LitBool, Path, Result, Type, TypePath, meta::ParseNestedMeta}; +use syn::{Ident, LitBool, Path, Result, Type, TypePath, meta::ParseNestedMeta}; use crate::{ - util::{generic_arguments, upper_camel_ident}, + generics::{Generic, generic_arguments}, + util::upper_camel_ident, validation::{Execution, Validation}, }; @@ -93,10 +94,14 @@ impl Validation for Custom { self.error_type.to_token_stream() } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { generic_arguments(&self.error_type) } + fn error_where_predicates(&self) -> Vec { + vec![] + } + fn expr(&self, execution: Execution, expr: &TokenStream) -> Option { let context_expr = self.context.then(|| quote!(, &context)); diff --git a/packages/fortifier-macros/src/validations/email_address.rs b/packages/fortifier-macros/src/validations/email_address.rs index 2b69d07..7eb4cc0 100644 --- a/packages/fortifier-macros/src/validations/email_address.rs +++ b/packages/fortifier-macros/src/validations/email_address.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{GenericArgument, Ident, LitBool, LitInt, Result, Type, meta::ParseNestedMeta}; +use syn::{Ident, LitBool, LitInt, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; pub struct EmailAddress { allow_display_text: bool, @@ -57,7 +60,11 @@ impl Validation for EmailAddress { quote!(::fortifier::EmailAddressError) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier-macros/src/validations/length.rs b/packages/fortifier-macros/src/validations/length.rs index c50ac96..368bc4d 100644 --- a/packages/fortifier-macros/src/validations/length.rs +++ b/packages/fortifier-macros/src/validations/length.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; #[derive(Default)] pub struct Length { @@ -55,7 +58,11 @@ impl Validation for Length { quote!(::fortifier::LengthError) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier-macros/src/validations/nested.rs b/packages/fortifier-macros/src/validations/nested.rs index cc49a0f..e576cfe 100644 --- a/packages/fortifier-macros/src/validations/nested.rs +++ b/packages/fortifier-macros/src/validations/nested.rs @@ -1,20 +1,30 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{GenericArgument, Ident, Result, Type, TypePath, meta::ParseNestedMeta}; +use syn::{GenericParam, Ident, Result, Type, TypePath, meta::ParseNestedMeta}; use crate::{ - attributes::enum_field_attributes, - util::generic_arguments, + generics::{Generic, generic_arguments}, + integrations::enum_field_attributes, validation::{Execution, Validation}, }; pub struct Nested { error_type: TypePath, + generic_params: Vec, + where_predicates: Vec, } impl Nested { - pub fn new(error_type: TypePath) -> Self { - Self { error_type } + pub fn new( + error_type: TypePath, + generic_params: Vec, + where_predicates: Vec, + ) -> Self { + Self { + error_type, + generic_params, + where_predicates, + } } } @@ -36,7 +46,11 @@ impl Validation for Nested { return Err(meta.error("missing `error_type` parameter")); }; - Ok(Nested { error_type }) + Ok(Nested { + error_type, + generic_params: vec![], + where_predicates: vec![], + }) } fn ident(&self) -> Ident { @@ -50,8 +64,15 @@ impl Validation for Nested { quote!(#attributes ::fortifier::ValidationErrors<#error_type>) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { generic_arguments(&self.error_type) + .into_iter() + .chain(self.generic_params.iter().cloned().map(Generic::Param)) + .collect() + } + + fn error_where_predicates(&self) -> Vec { + self.where_predicates.clone() } fn expr(&self, execution: Execution, expr: &TokenStream) -> Option { diff --git a/packages/fortifier-macros/src/validations/phone_number.rs b/packages/fortifier-macros/src/validations/phone_number.rs index ed52984..f28313f 100644 --- a/packages/fortifier-macros/src/validations/phone_number.rs +++ b/packages/fortifier-macros/src/validations/phone_number.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; #[derive(Default)] pub struct PhoneNumber { @@ -42,7 +45,11 @@ impl Validation for PhoneNumber { quote!(::fortifier::PhoneNumberError) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier-macros/src/validations/range.rs b/packages/fortifier-macros/src/validations/range.rs index 1867e68..d3d7ef7 100644 --- a/packages/fortifier-macros/src/validations/range.rs +++ b/packages/fortifier-macros/src/validations/range.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; pub struct Range { r#type: Type, @@ -68,7 +71,11 @@ impl Validation for Range { quote!(::fortifier::RangeError<#r#type>) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier-macros/src/validations/regex.rs b/packages/fortifier-macros/src/validations/regex.rs index 7246435..0f915db 100644 --- a/packages/fortifier-macros/src/validations/regex.rs +++ b/packages/fortifier-macros/src/validations/regex.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; pub struct Regex { expression: Expr, @@ -41,7 +44,11 @@ impl Validation for Regex { quote!(::fortifier::RegexError) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier-macros/src/validations/url.rs b/packages/fortifier-macros/src/validations/url.rs index 0169bd0..a3f8c24 100644 --- a/packages/fortifier-macros/src/validations/url.rs +++ b/packages/fortifier-macros/src/validations/url.rs @@ -1,8 +1,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{GenericArgument, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Ident, Result, Type, meta::ParseNestedMeta}; -use crate::validation::{Execution, Validation}; +use crate::{ + generics::Generic, + validation::{Execution, Validation}, +}; #[derive(Default)] pub struct Url {} @@ -20,7 +23,11 @@ impl Validation for Url { quote!(::fortifier::UrlError) } - fn error_generic_arguments(&self) -> Vec { + fn error_generics(&self) -> Vec { + vec![] + } + + fn error_where_predicates(&self) -> Vec { vec![] } diff --git a/packages/fortifier/src/integrations.rs b/packages/fortifier/src/integrations.rs index e90509d..b0f2c8e 100644 --- a/packages/fortifier/src/integrations.rs +++ b/packages/fortifier/src/integrations.rs @@ -1,11 +1,2 @@ #[cfg(feature = "serde")] pub mod serde; - -#[doc(hidden)] -pub mod external { - #[cfg(feature = "serde")] - pub use serde; - - #[cfg(feature = "utoipa")] - pub use utoipa; -} diff --git a/packages/fortifier/src/lib.rs b/packages/fortifier/src/lib.rs index 303f7a4..83b6547 100644 --- a/packages/fortifier/src/lib.rs +++ b/packages/fortifier/src/lib.rs @@ -9,6 +9,7 @@ mod validate; mod validations; pub use error::*; +#[allow(unused_imports)] pub use integrations::*; pub use validate::*; pub use validations::*; diff --git a/packages/fortifier/src/validate.rs b/packages/fortifier/src/validate.rs index 0e955a2..79028cb 100644 --- a/packages/fortifier/src/validate.rs +++ b/packages/fortifier/src/validate.rs @@ -16,7 +16,7 @@ pub trait ValidateWithContext { type Context: Send + Sync; /// Validation error. - type Error: Error; + type Error: Error + Send + Sync; /// Validate schema using all validators with context. fn validate_with_context(