use syn::punctuated::Punctuated;
use syn::{Attribute, Expr, ExprLit, Lit, Token, Type};
struct MetaEntry {
key: syn::Ident,
value: Option<Expr>,
}
impl syn::parse::Parse for MetaEntry {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key: syn::Ident = input.parse()?;
let value = if input.peek(Token![=]) {
let _: Token![=] = input.parse()?;
Some(input.parse()?)
} else {
None
};
Ok(Self { key, value })
}
}
pub struct ToolAttr {
entries: Vec<MetaEntry>,
}
impl ToolAttr {
pub fn collect(attrs: &[Attribute], ident: &str) -> syn::Result<Self> {
let mut entries = Vec::new();
for attr in attrs {
if !attr.path().is_ident(ident) {
continue;
}
let parsed =
attr.parse_args_with(Punctuated::<MetaEntry, Token![,]>::parse_terminated)?;
entries.extend(parsed);
}
Ok(Self { entries })
}
fn get(&self, key: &str) -> Option<&Expr> {
self.entries
.iter()
.find(|e| e.key == key)
.and_then(|e| e.value.as_ref())
}
pub fn require_string(&self, key: &str, span: proc_macro2::Span) -> syn::Result<String> {
self.get(key).map_or_else(
|| {
Err(syn::Error::new(
span,
format!("#[tool(...)] is missing the required `{key} = \"...\"`"),
))
},
|expr| as_string(expr, key),
)
}
pub fn opt_string(&self, key: &str) -> syn::Result<Option<String>> {
self.get(key).map(|e| as_string(e, key)).transpose()
}
pub fn require_type(&self, key: &str, span: proc_macro2::Span) -> syn::Result<Type> {
self.get(key).map_or_else(
|| {
Err(syn::Error::new(
span,
format!("#[tool(...)] is missing the required `{key} = <Type>`"),
))
},
as_type,
)
}
pub fn opt_type(&self, key: &str) -> syn::Result<Option<Type>> {
self.get(key).map(as_type).transpose()
}
#[must_use]
pub fn opt_expr(&self, key: &str) -> Option<Expr> {
self.get(key).cloned()
}
}
fn as_string(expr: &Expr, key: &str) -> syn::Result<String> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => Ok(s.value()),
other => Err(syn::Error::new_spanned(
other,
format!("`{key}` must be a string literal"),
)),
}
}
fn as_type(expr: &Expr) -> syn::Result<Type> {
syn::parse2(quote::quote!(#expr))
}