#![allow(dead_code)] #![allow(clippy::cast_precision_loss)]
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Attribute, Expr, ExprLit, Lit, Meta, MetaNameValue};
#[derive(Default)]
pub struct ParamAttrs {
pub title: Option<String>,
pub description: Option<String>,
pub deprecated: bool,
pub exclude: bool,
pub example: Option<TokenStream2>,
pub ge: Option<f64>,
pub le: Option<f64>,
pub gt: Option<f64>,
pub lt: Option<f64>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub alias: Option<String>,
pub validation_alias: Option<String>,
pub serialization_alias: Option<String>,
}
impl ParamAttrs {
#[allow(clippy::too_many_lines)]
pub fn from_attributes(attrs: &[Attribute]) -> Self {
let mut result = Self::default();
for attr in attrs {
if !attr.path().is_ident("param") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("title") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.title = Some(s.value());
}
}
} else if meta.path.is_ident("description") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.description = Some(s.value());
}
}
} else if meta.path.is_ident("deprecated") {
result.deprecated = true;
} else if meta.path.is_ident("exclude") {
result.exclude = true;
} else if meta.path.is_ident("example") {
if let Ok(value) = meta.value() {
if let Ok(expr) = value.parse::<syn::Expr>() {
result.example = Some(quote! { #expr });
}
}
} else if meta.path.is_ident("ge") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Float(f)) = value.parse::<Lit>() {
result.ge = f.base10_parse().ok();
} else if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.ge = i.base10_parse::<i64>().ok().map(|v| v as f64);
}
}
} else if meta.path.is_ident("le") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Float(f)) = value.parse::<Lit>() {
result.le = f.base10_parse().ok();
} else if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.le = i.base10_parse::<i64>().ok().map(|v| v as f64);
}
}
} else if meta.path.is_ident("gt") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Float(f)) = value.parse::<Lit>() {
result.gt = f.base10_parse().ok();
} else if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.gt = i.base10_parse::<i64>().ok().map(|v| v as f64);
}
}
} else if meta.path.is_ident("lt") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Float(f)) = value.parse::<Lit>() {
result.lt = f.base10_parse().ok();
} else if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.lt = i.base10_parse::<i64>().ok().map(|v| v as f64);
}
}
} else if meta.path.is_ident("min_length") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.min_length = i.base10_parse().ok();
}
}
} else if meta.path.is_ident("max_length") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Int(i)) = value.parse::<Lit>() {
result.max_length = i.base10_parse().ok();
}
}
} else if meta.path.is_ident("pattern") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.pattern = Some(s.value());
}
}
} else if meta.path.is_ident("alias") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.alias = Some(s.value());
}
}
} else if meta.path.is_ident("validation_alias") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.validation_alias = Some(s.value());
}
}
} else if meta.path.is_ident("serialization_alias") {
if let Ok(value) = meta.value() {
if let Ok(Lit::Str(s)) = value.parse::<Lit>() {
result.serialization_alias = Some(s.value());
}
}
}
Ok(())
});
}
if result.description.is_none() {
result.description = extract_doc_comment(attrs);
}
result
}
pub fn to_param_meta_tokens(&self) -> TokenStream2 {
let title = match &self.title {
Some(t) => quote! { .title(#t) },
None => quote! {},
};
let description = match &self.description {
Some(d) => quote! { .description(#d) },
None => quote! {},
};
let deprecated = if self.deprecated {
quote! { .deprecated() }
} else {
quote! {}
};
let exclude = if self.exclude {
quote! { .exclude_from_schema() }
} else {
quote! {}
};
let example = match &self.example {
Some(e) => quote! { .example(serde_json::json!(#e)) },
None => quote! {},
};
let ge = match self.ge {
Some(v) => quote! { .ge(#v) },
None => quote! {},
};
let le = match self.le {
Some(v) => quote! { .le(#v) },
None => quote! {},
};
let gt = match self.gt {
Some(v) => quote! { .gt(#v) },
None => quote! {},
};
let lt = match self.lt {
Some(v) => quote! { .lt(#v) },
None => quote! {},
};
let min_length = match self.min_length {
Some(v) => quote! { .min_length(#v) },
None => quote! {},
};
let max_length = match self.max_length {
Some(v) => quote! { .max_length(#v) },
None => quote! {},
};
let pattern = match &self.pattern {
Some(p) => quote! { .pattern(#p) },
None => quote! {},
};
let alias = match &self.alias {
Some(a) => quote! { .alias(#a) },
None => quote! {},
};
let validation_alias = match &self.validation_alias {
Some(a) => quote! { .validation_alias(#a) },
None => quote! {},
};
let serialization_alias = match &self.serialization_alias {
Some(a) => quote! { .serialization_alias(#a) },
None => quote! {},
};
quote! {
fastapi_openapi::ParamMeta::new()
#title
#description
#deprecated
#exclude
#example
#ge
#le
#gt
#lt
#min_length
#max_length
#pattern
#alias
#validation_alias
#serialization_alias
}
}
}
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
let docs: Vec<String> = attrs
.iter()
.filter_map(|attr| {
if !attr.path().is_ident("doc") {
return None;
}
match &attr.meta {
Meta::NameValue(MetaNameValue {
value:
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}),
..
}) => Some(s.value().trim().to_string()),
_ => None,
}
})
.collect();
if docs.is_empty() {
None
} else {
Some(docs.join("\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_param_attrs_default() {
let attrs = ParamAttrs::default();
assert!(attrs.title.is_none());
assert!(attrs.description.is_none());
assert!(!attrs.deprecated);
assert!(!attrs.exclude);
assert!(attrs.alias.is_none());
assert!(attrs.validation_alias.is_none());
assert!(attrs.serialization_alias.is_none());
}
#[test]
fn test_param_attrs_alias_defaults() {
let attrs = ParamAttrs::default();
assert!(attrs.alias.is_none());
assert!(attrs.validation_alias.is_none());
assert!(attrs.serialization_alias.is_none());
}
#[test]
fn test_param_attrs_alias_set() {
let attrs = ParamAttrs {
alias: Some("q".to_string()),
validation_alias: Some("query_param".to_string()),
serialization_alias: Some("search_query".to_string()),
..Default::default()
};
assert_eq!(attrs.alias.as_deref(), Some("q"));
assert_eq!(attrs.validation_alias.as_deref(), Some("query_param"));
assert_eq!(attrs.serialization_alias.as_deref(), Some("search_query"));
}
#[test]
fn test_to_param_meta_tokens_with_alias() {
let attrs = ParamAttrs {
alias: Some("x-custom-token".to_string()),
..Default::default()
};
let tokens = attrs.to_param_meta_tokens();
let token_string = tokens.to_string();
assert!(token_string.contains("alias"));
}
}