diff --git a/packages/fortifier-macros/src/validate.rs b/packages/fortifier-macros/src/validate.rs index 9c64cc7..b13f029 100644 --- a/packages/fortifier-macros/src/validate.rs +++ b/packages/fortifier-macros/src/validate.rs @@ -3,6 +3,7 @@ mod r#enum; mod field; mod fields; mod r#struct; +mod r#type; mod r#union; use proc_macro2::TokenStream; diff --git a/packages/fortifier-macros/src/validate/attributes.rs b/packages/fortifier-macros/src/validate/attributes.rs index a195a81..cdbf04b 100644 --- a/packages/fortifier-macros/src/validate/attributes.rs +++ b/packages/fortifier-macros/src/validate/attributes.rs @@ -2,6 +2,7 @@ use proc_macro2::TokenStream; use quote::quote; pub fn enum_attributes() -> TokenStream { + #[allow(unused_mut)] let mut attributes: Vec = vec![]; #[cfg(feature = "serde")] @@ -12,7 +13,6 @@ pub fn enum_attributes() -> TokenStream { attributes.push(quote! { #[derive(serde::Deserialize, serde::Serialize)] #[serde( - // TODO: Tag? tag = "path", rename_all = "camelCase", rename_all_fields = "camelCase" diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index 28a50e7..e41b3c1 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -4,7 +4,7 @@ use quote::{ToTokens, format_ident, quote}; use syn::{Field, Ident, Result, Visibility}; use crate::{ - validate::attributes::enum_attributes, + validate::{attributes::enum_attributes, r#type::should_validate_type}, validation::{Execution, Validation}, validations::{Custom, Email, Length, Regex, Url}, }; @@ -60,6 +60,7 @@ impl<'a> ValidateField<'a> { error_type_ident, validations: vec![], }; + let mut skip = false; for attr in &field.attrs { if attr.path().is_ident("validate") { @@ -83,6 +84,10 @@ impl<'a> ValidateField<'a> { } else if meta.path.is_ident("url") { result.validations.push(Box::new(Url::parse(&meta)?)); + Ok(()) + } else if meta.path.is_ident("skip") { + skip = true; + Ok(()) } else { Err(meta.error("unknown parameter")) @@ -91,6 +96,10 @@ impl<'a> ValidateField<'a> { } } + if !skip && should_validate_type(&field.ty) { + // TODO: Nested validation + } + Ok(result) } diff --git a/packages/fortifier-macros/src/validate/type.rs b/packages/fortifier-macros/src/validate/type.rs new file mode 100644 index 0000000..269661a --- /dev/null +++ b/packages/fortifier-macros/src/validate/type.rs @@ -0,0 +1,191 @@ +use syn::{GenericArgument, Path, PathArguments, Type, TypeParamBound}; + +const PRIMITIVE_AND_BUILT_IN_TYPES: [&str; 18] = [ + "bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", + "f32", "f64", "char", "str", "String", +]; + +const CONTAINER_TYPES: [&str; 20] = [ + "Arc", + "BTreeMap", + "BTreeSet", + "HashMap", + "HashSet", + "LinkedList", + "Option", + "Rc", + "Vec", + "VecDeque", + "std::collections::BTreeMap", + "std::collections::BTreeSet", + "std::collections::HashMap", + "std::collections::HashSet", + "std::collections::LinkedList", + "std::collections::VecDeque", + "std::option::Option", + "std::rc::Rc", + "std::sync::Arc", + "std::vec::Vec", +]; + +fn path_to_string(path: &Path) -> String { + // TODO: This is probably slow, replace with comparisons. + path.segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join("::") +} + +fn is_validate_path(path: &Path) -> bool { + let path_string = path_to_string(path); + path_string == "Validate" || path_string == "fortifier::Validate" +} + +fn should_validate_generic_argument(arg: &GenericArgument) -> bool { + match arg { + GenericArgument::Lifetime(_) => true, + GenericArgument::Type(r#type) => should_validate_type(r#type), + GenericArgument::Const(_expr) => todo!(), + GenericArgument::AssocType(_assoc_type) => todo!(), + GenericArgument::AssocConst(_assoc_const) => todo!(), + GenericArgument::Constraint(_constraint) => todo!(), + _ => true, + } +} + +fn should_validate_path(path: &Path) -> bool { + if let Some(ident) = path.get_ident() { + return !PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str()); + } + let path_string = path_to_string(path); + + if CONTAINER_TYPES.contains(&path_string.as_str()) + && let Some(segment) = path.segments.last() + && let PathArguments::AngleBracketed(arguments) = &segment.arguments + && !arguments.args.iter().all(should_validate_generic_argument) + { + return false; + } + + true +} + +pub fn should_validate_type(r#type: &Type) -> bool { + match r#type { + Type::Array(r#type) => should_validate_type(&r#type.elem), + Type::BareFn(_) => false, + Type::Group(r#type) => should_validate_type(&r#type.elem), + Type::ImplTrait(r#type) => r#type.bounds.iter().any( + |bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)), + ), + Type::Infer(_) => true, + Type::Macro(_) => true, + Type::Never(_) => false, + Type::Paren(r#type) => should_validate_type(&r#type.elem), + Type::Path(r#type) => should_validate_path(&r#type.path), + Type::Ptr(r#type) => should_validate_type(&r#type.elem), + Type::Reference(r#type) => should_validate_type(&r#type.elem), + Type::Slice(r#type) => should_validate_type(&r#type.elem), + Type::TraitObject(r#type) => r#type.bounds.iter().any( + |bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)), + ), + Type::Tuple(r#type) => { + !r#type.elems.is_empty() && r#type.elems.iter().all(should_validate_type) + } + Type::Verbatim(_) => false, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use proc_macro2::TokenStream; + use quote::quote; + + use super::should_validate_type; + + fn validate(tokens: TokenStream) -> bool { + should_validate_type(&syn::parse2(tokens).expect("Type should be valid.")) + } + + #[test] + fn should_validate() { + assert!(validate(quote!(&T))); + assert!(validate(quote!(T))); + + assert!(validate(quote!((T, T)))); + assert!(validate(quote!((A, B, C)))); + + assert!(validate(quote!([T]))); + assert!(validate(quote!([T; 3]))); + assert!(validate(quote!([&T]))); + assert!(validate(quote!([&T; 3]))); + assert!(validate(quote!(&[T]))); + assert!(validate(quote!(&[T; 3]))); + + assert!(validate(quote!(Arc))); + assert!(validate(quote!(BTreeSet))); + assert!(validate(quote!(BTreeMap))); + assert!(validate(quote!(HashSet))); + assert!(validate(quote!(HashMap))); + assert!(validate(quote!(LinkedList))); + assert!(validate(quote!(Option))); + assert!(validate(quote!(Option>))); + assert!(validate(quote!(Rc))); + assert!(validate(quote!(Vec))); + assert!(validate(quote!(VecDeque))); + + assert!(validate(quote!(impl Validate))); + assert!(validate(quote!(impl fortifier::Validate))); + assert!(validate(quote!(dyn Validate))); + assert!(validate(quote!(dyn ::fortifier::Validate))); + } + + #[test] + fn should_not_validate() { + assert!(!validate(quote!(bool))); + assert!(!validate(quote!(i8))); + assert!(!validate(quote!(i16))); + assert!(!validate(quote!(i32))); + assert!(!validate(quote!(i64))); + assert!(!validate(quote!(i128))); + assert!(!validate(quote!(isize))); + assert!(!validate(quote!(u8))); + assert!(!validate(quote!(u16))); + assert!(!validate(quote!(u32))); + assert!(!validate(quote!(u64))); + assert!(!validate(quote!(u128))); + assert!(!validate(quote!(usize))); + assert!(!validate(quote!(f32))); + assert!(!validate(quote!(f64))); + assert!(!validate(quote!(char))); + assert!(!validate(quote!(&str))); + assert!(!validate(quote!(String))); + + assert!(!validate(quote!(()))); + assert!(!validate(quote!((bool, bool)))); + assert!(!validate(quote!((usize, usize, usize)))); + assert!(!validate(quote!((usize, &str)))); + + assert!(!validate(quote!([isize]))); + assert!(!validate(quote!([&str; 3]))); + assert!(!validate(quote!(&[isize]))); + assert!(!validate(quote!(&[&str; 3]))); + + assert!(!validate(quote!(Arc<&str>))); + assert!(!validate(quote!(BTreeSet))); + assert!(!validate(quote!(BTreeMap))); + assert!(!validate(quote!(HashSet<&str>))); + assert!(!validate(quote!(HashMap<&str, &str>))); + assert!(!validate(quote!(LinkedList))); + assert!(!validate(quote!(Option))); + assert!(!validate(quote!(Option>))); + assert!(!validate(quote!(Rc<&str>))); + assert!(!validate(quote!(Vec))); + assert!(!validate(quote!(VecDeque))); + + assert!(!validate(quote!(impl Serialize))); + assert!(!validate(quote!(dyn Serialize))); + } +} diff --git a/packages/fortifier-macros/tests/derive/enum_named_pass.rs b/packages/fortifier-macros/tests/derive/enum_named_pass.rs index 6946dfb..93fb43e 100644 --- a/packages/fortifier-macros/tests/derive/enum_named_pass.rs +++ b/packages/fortifier-macros/tests/derive/enum_named_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] enum ChangeEmailAddressRelation { @@ -19,7 +17,7 @@ enum ChangeEmailAddressRelation { }, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create { email_address: "john@doe.com".to_owned(), }; diff --git a/packages/fortifier-macros/tests/derive/enum_unit_pass.rs b/packages/fortifier-macros/tests/derive/enum_unit_pass.rs index 20f19f7..da99c1e 100644 --- a/packages/fortifier-macros/tests/derive/enum_unit_pass.rs +++ b/packages/fortifier-macros/tests/derive/enum_unit_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] enum ChangeEmailAddressRelation { @@ -9,7 +7,7 @@ enum ChangeEmailAddressRelation { Delete, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create; data.validate_sync()?; diff --git a/packages/fortifier-macros/tests/derive/enum_unnamed_pass.rs b/packages/fortifier-macros/tests/derive/enum_unnamed_pass.rs index c07374d..976a4c4 100644 --- a/packages/fortifier-macros/tests/derive/enum_unnamed_pass.rs +++ b/packages/fortifier-macros/tests/derive/enum_unnamed_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] enum ChangeEmailAddressRelation { @@ -9,7 +7,7 @@ enum ChangeEmailAddressRelation { Delete(String), } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create("john@doe.com".to_owned()); data.validate_sync()?; diff --git a/packages/fortifier-macros/tests/derive/struct_named_generics_pass.rs b/packages/fortifier-macros/tests/derive/struct_named_generics_pass.rs index 2002eee..a5d3e25 100644 --- a/packages/fortifier-macros/tests/derive/struct_named_generics_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_named_generics_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::{Validate, ValidateEmail, ValidateLength}; +use fortifier::{Validate, ValidateEmail, ValidateLength, ValidationErrors}; #[derive(Validate)] struct CreateUser> { @@ -11,7 +9,7 @@ struct CreateUser> { name: N, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email: "john@doe.com", name: "John Doe", diff --git a/packages/fortifier-macros/tests/derive/struct_named_lifetimes_pass.rs b/packages/fortifier-macros/tests/derive/struct_named_lifetimes_pass.rs index 90f8d15..b0a452c 100644 --- a/packages/fortifier-macros/tests/derive/struct_named_lifetimes_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_named_lifetimes_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] struct CreateUser<'a, 'b> { @@ -11,7 +9,7 @@ struct CreateUser<'a, 'b> { name: &'b str, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email: "john@doe.com", name: "John Doe", diff --git a/packages/fortifier-macros/tests/derive/struct_named_pass.rs b/packages/fortifier-macros/tests/derive/struct_named_pass.rs index abe2f6d..621edcb 100644 --- a/packages/fortifier-macros/tests/derive/struct_named_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_named_pass.rs @@ -1,6 +1,4 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] struct CreateUser { @@ -11,7 +9,7 @@ struct CreateUser { name: String, } -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email: "john@doe.com".to_owned(), name: "John Doe".to_owned(), diff --git a/packages/fortifier-macros/tests/derive/struct_unit_pass.rs b/packages/fortifier-macros/tests/derive/struct_unit_pass.rs index 25200b3..7710ebf 100644 --- a/packages/fortifier-macros/tests/derive/struct_unit_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_unit_pass.rs @@ -1,11 +1,11 @@ -use std::error::Error; +use std::convert::Infallible; -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] struct CreateUser; -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser; data.validate_sync()?; diff --git a/packages/fortifier-macros/tests/derive/struct_unnamed_generics_pass.rs b/packages/fortifier-macros/tests/derive/struct_unnamed_generics_pass.rs index eee76c5..d5af8d0 100644 --- a/packages/fortifier-macros/tests/derive/struct_unnamed_generics_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_unnamed_generics_pass.rs @@ -1,11 +1,9 @@ -use std::error::Error; - -use fortifier::{Validate, ValidateLength}; +use fortifier::{Validate, ValidateLength, ValidationErrors}; #[derive(Validate)] struct CreateUser>(#[validate(length(min = 1, max = 256))] N); -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe"); data.validate_sync()?; diff --git a/packages/fortifier-macros/tests/derive/struct_unnamed_lifetimes_pass.rs b/packages/fortifier-macros/tests/derive/struct_unnamed_lifetimes_pass.rs index 250b0d6..7e6afac 100644 --- a/packages/fortifier-macros/tests/derive/struct_unnamed_lifetimes_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_unnamed_lifetimes_pass.rs @@ -1,11 +1,9 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] struct CreateUser<'a>(#[validate(length(min = 1, max = 256))] &'a str); -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe"); data.validate_sync()?; diff --git a/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs b/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs index 7cc79c9..0a0e8ba 100644 --- a/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs +++ b/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs @@ -1,11 +1,9 @@ -use std::error::Error; - -use fortifier::Validate; +use fortifier::{Validate, ValidationErrors}; #[derive(Validate)] struct CreateUser(#[validate(length(min = 1, max = 256))] String); -fn main() -> Result<(), Box> { +fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe".to_owned()); data.validate_sync()?;