use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{Attribute, Error, Expr, GenericArgument, Lit, PathArguments, Result};
pub fn find_attr<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
attrs.iter().find(|attr| attr.path().is_ident(name))
}
pub fn extract_generic(ty: &syn::Type, name: &str) -> Option<syn::Type> {
let check_name = |path: &syn::Path| {
path.leading_colon.is_none()
&& path.segments.len() == 1
&& path.segments.first().unwrap().ident == name
};
match ty {
syn::Type::Path(path) if path.qself.is_none() && check_name(&path.path) => {
let arguments = &path.path.segments.first().unwrap().arguments;
let arg = match arguments {
PathArguments::AngleBracketed(params) if params.args.len() == 1 => {
params.args.first().unwrap()
}
_ => return None,
};
match arg {
GenericArgument::Type(ty) => Some(ty.clone()),
_ => None,
}
}
_ => None,
}
}
pub fn parse_doc(attrs: &[Attribute], span: Span) -> Result<String> {
let Some(attr) = find_attr(attrs, "doc") else {
return Err(Error::new(
span,
"description is required (documentation comment or `desc` attribute)",
));
};
let meta = attr.meta.require_name_value()?;
let Expr::Lit(expr) = &meta.value else {
return Err(Error::new_spanned(&meta.value, "expected string literal"));
};
let Lit::Str(lit) = &expr.lit else {
return Err(Error::new_spanned(&expr.lit, "expected string literal"));
};
let doc = lit.value().trim().to_string();
match doc.chars().count() {
1..=100 => Ok(doc),
_ => Err(Error::new_spanned(
lit,
"description must be between 1 and 100 characters",
)),
}
}
pub fn optional<T>(value: Option<T>) -> TokenStream
where
T: ToTokens,
{
match value {
Some(value) => quote! { ::std::option::Option::Some(#value) },
None => quote! {::std::option::Option::None },
}
}