diff --git a/Cargo.lock b/Cargo.lock index fe49d19..3c4d2fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,7 @@ name = "fortifier-example" version = "0.0.1" dependencies = [ "fortifier", + "tokio", ] [[package]] @@ -213,6 +214,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "potential_utf" version = "0.1.4" @@ -357,6 +364,27 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index 7ca6768..3ebb78f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ version = "0.0.1" [workspace.dependencies] fortifier = { path = "./packages/fortifier", version = "0.0.1" } fortifier-macros = { path = "./packages/fortifier-macros", version = "0.0.1" } +tokio = "1.48.0" [workspace.lints.rust] unsafe_code = "deny" diff --git a/example/Cargo.toml b/example/Cargo.toml index a33ae5a..9c66441 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -10,6 +10,7 @@ version.workspace = true [dependencies] fortifier.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [lints] workspace = true diff --git a/example/src/main.rs b/example/src/main.rs index c61972c..312c56f 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -12,16 +12,32 @@ struct CreateUser { #[validate(url)] url: String, + + #[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))] + locales: Vec, +} + +#[derive(Debug)] +struct OneLocaleRequiredError; + +fn validate_one_locale_required(locales: &[String]) -> Result<(), OneLocaleRequiredError> { + if locales.is_empty() { + Err(OneLocaleRequiredError) + } else { + Ok(()) + } } -fn main() -> Result<(), Box> { +#[tokio::main] +async fn main() -> Result<(), Box> { let data = CreateUser { email: "john@doe.com".to_owned(), name: "John Doe".to_owned(), url: "https://john.doe.com".to_owned(), + locales: vec!["en_GB".to_owned()], }; - data.validate_sync()?; + data.validate().await?; Ok(()) } diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index e722a69..513f337 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -4,7 +4,7 @@ use syn::{Field, Result}; use crate::{ validation::Validation, - validations::{Email, Length, Url}, + validations::{Custom, Email, Length, Url}, }; pub struct ValidateField { @@ -22,7 +22,11 @@ impl ValidateField { for attr in &field.attrs { if attr.path().is_ident("validate") { attr.parse_nested_meta(|meta| { - if meta.path.is_ident("email") { + if meta.path.is_ident("custom") { + result.validations.push(Box::new(Custom::parse(&meta)?)); + + Ok(()) + } else if meta.path.is_ident("email") { result.validations.push(Box::new(Email::parse(&meta)?)); Ok(()) diff --git a/packages/fortifier-macros/src/validations.rs b/packages/fortifier-macros/src/validations.rs index 2909a91..dbaa948 100644 --- a/packages/fortifier-macros/src/validations.rs +++ b/packages/fortifier-macros/src/validations.rs @@ -1,7 +1,9 @@ +mod custom; mod email; mod length; mod url; +pub use custom::*; pub use email::*; pub use length::*; pub use url::*; diff --git a/packages/fortifier-macros/src/validations/custom.rs b/packages/fortifier-macros/src/validations/custom.rs new file mode 100644 index 0000000..aeeb779 --- /dev/null +++ b/packages/fortifier-macros/src/validations/custom.rs @@ -0,0 +1,77 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{LitBool, Path, Result, Type, meta::ParseNestedMeta}; + +use crate::validation::Validation; + +pub struct Custom { + is_async: bool, + error_type: Type, + function_path: Path, +} + +impl Validation for Custom { + fn parse(meta: &ParseNestedMeta<'_>) -> Result { + let mut is_async = false; + let mut error_type: Option = None; + let mut function_path: Option = None; + + meta.parse_nested_meta(|meta| { + if meta.path.is_ident("async") { + if let Ok(value) = meta.value() { + let lit: LitBool = value.parse()?; + is_async = lit.value; + } else { + is_async = true; + } + + Ok(()) + } else if meta.path.is_ident("error") { + error_type = Some(meta.value()?.parse()?); + + Ok(()) + } else if meta.path.is_ident("function") { + function_path = Some(meta.value()?.parse()?); + + Ok(()) + } else { + Err(meta.error("unknown parameter")) + } + })?; + + let Some(error_type) = error_type else { + return Err(meta.error("missing error parameter")); + }; + let Some(function_path) = function_path else { + return Err(meta.error("missing function parameter")); + }; + + Ok(Custom { + is_async, + error_type, + function_path, + }) + } + + fn is_async(&self) -> bool { + self.is_async + } + + fn error_type(&self) -> TokenStream { + self.error_type.to_token_stream() + } + + fn tokens(&self, expr: &TokenStream) -> TokenStream { + let function_path = &self.function_path; + + if self.is_async { + quote! { + #function_path(&#expr).await + } + } else { + quote! { + #function_path(&#expr) + } + } + } +}