use proc_macro2::TokenStream;
use quote::quote;
use syn::Attribute;
#[derive(Debug, Clone, Default)]
pub struct ValidationConfig {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub minimum: Option<i64>,
pub maximum: Option<i64>,
pub email: bool,
pub url: bool,
pub pattern: Option<String>,
pub raw_attrs: Vec<TokenStream>
}
impl ValidationConfig {
#[must_use]
#[allow(dead_code)] pub fn has_validation(&self) -> bool {
self.min_length.is_some()
|| self.max_length.is_some()
|| self.minimum.is_some()
|| self.maximum.is_some()
|| self.email
|| self.url
|| self.pattern.is_some()
}
#[must_use]
#[allow(dead_code)] pub fn to_schema_attrs(&self) -> TokenStream {
let mut attrs = Vec::new();
if let Some(min) = self.min_length {
attrs.push(quote! { min_length = #min });
}
if let Some(max) = self.max_length {
attrs.push(quote! { max_length = #max });
}
if let Some(min) = self.minimum {
attrs.push(quote! { minimum = #min });
}
if let Some(max) = self.maximum {
attrs.push(quote! { maximum = #max });
}
if self.email {
attrs.push(quote! { format = "email" });
}
if self.url {
attrs.push(quote! { format = "uri" });
}
if let Some(ref pattern) = self.pattern {
attrs.push(quote! { pattern = #pattern });
}
if attrs.is_empty() {
TokenStream::new()
} else {
quote! { #(, #attrs)* }
}
}
}
pub fn parse_validation_attrs(attrs: &[Attribute]) -> ValidationConfig {
let mut config = ValidationConfig::default();
for attr in attrs {
if !attr.path().is_ident("validate") {
continue;
}
config.raw_attrs.push(quote! { #attr });
let _ = attr.parse_nested_meta(|meta| {
let path_str = meta.path.get_ident().map(|i| i.to_string());
match path_str.as_deref() {
Some("length") => {
meta.parse_nested_meta(|nested| {
let nested_path = nested.path.get_ident().map(|i| i.to_string());
match nested_path.as_deref() {
Some("min") => {
let value: syn::LitInt = nested.value()?.parse()?;
config.min_length = Some(value.base10_parse()?);
}
Some("max") => {
let value: syn::LitInt = nested.value()?.parse()?;
config.max_length = Some(value.base10_parse()?);
}
_ => {}
}
Ok(())
})?;
}
Some("range") => {
meta.parse_nested_meta(|nested| {
let nested_path = nested.path.get_ident().map(|i| i.to_string());
match nested_path.as_deref() {
Some("min") => {
let value: syn::LitInt = nested.value()?.parse()?;
config.minimum = Some(value.base10_parse()?);
}
Some("max") => {
let value: syn::LitInt = nested.value()?.parse()?;
config.maximum = Some(value.base10_parse()?);
}
_ => {}
}
Ok(())
})?;
}
Some("email") => {
config.email = true;
}
Some("url") => {
config.url = true;
}
Some("regex") => {
let value: syn::LitStr = meta.value()?.parse()?;
config.pattern = Some(value.value());
}
_ => {}
}
Ok(())
});
}
config
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_attrs(input: &str) -> Vec<Attribute> {
let item: syn::ItemStruct = syn::parse_str(input).unwrap();
item.fields
.iter()
.next()
.map(|f| f.attrs.clone())
.unwrap_or_default()
}
#[test]
fn parse_length_min_max() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(length(min = 1, max = 255))]
name: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert_eq!(config.min_length, Some(1));
assert_eq!(config.max_length, Some(255));
}
#[test]
fn parse_email() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(email)]
email: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert!(config.email);
}
#[test]
fn parse_url() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(url)]
website: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert!(config.url);
}
#[test]
fn parse_range() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(range(min = 0, max = 100))]
score: i32,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert_eq!(config.minimum, Some(0));
assert_eq!(config.maximum, Some(100));
}
#[test]
fn parse_multiple_validators() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(length(min = 5))]
#[validate(email)]
email: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert_eq!(config.min_length, Some(5));
assert!(config.email);
}
#[test]
fn no_validation() {
let attrs = parse_attrs(
r#"
struct Foo {
#[field(create)]
name: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert!(!config.has_validation());
}
#[test]
fn has_validation_true() {
let attrs = parse_attrs(
r#"
struct Foo {
#[validate(email)]
email: String,
}
"#
);
let config = parse_validation_attrs(&attrs);
assert!(config.has_validation());
}
#[test]
fn schema_attrs_generation() {
let config = ValidationConfig {
min_length: Some(1),
max_length: Some(100),
email: true,
..Default::default()
};
let attrs = config.to_schema_attrs();
let attrs_str = attrs.to_string();
assert!(attrs_str.contains("min_length"));
assert!(attrs_str.contains("max_length"));
assert!(attrs_str.contains("email"));
}
}