use proc_macro2::TokenStream as TokenStream2;
use syn::{Attribute, DeriveInput, Expr, FnArg, Lit, Meta, Pat, punctuated::Punctuated};
#[derive(Debug, Clone, Default)]
pub(crate) struct ToolMeta {
pub(crate) name: String,
pub(crate) description: String,
}
pub(crate) fn parse_tool_meta_tokens(args: &TokenStream2) -> ToolMeta {
let mut name = String::new();
let mut description = String::new();
if args.is_empty() {
return ToolMeta::default();
}
let tokens: Vec<proc_macro2::TokenTree> = args.clone().into_iter().collect();
let mut i = 0;
while i < tokens.len() {
if let proc_macro2::TokenTree::Ident(ident) = &tokens[i] {
let ident_name = ident.to_string();
if i + 1 < tokens.len()
&& let proc_macro2::TokenTree::Punct(p) = &tokens[i + 1]
&& p.as_char() == '='
{
if i + 2 < tokens.len()
&& let proc_macro2::TokenTree::Literal(lit) = &tokens[i + 2]
{
let lit_str = lit.to_string();
let val = lit_str.trim_matches(|c| c == '"' || c == '\'').to_string();
if ident_name == "name" {
name = val;
} else if ident_name == "description" {
description = val;
}
}
}
}
i += 1;
}
ToolMeta { name, description }
}
pub(crate) fn extract_helper_meta(attrs: &[Attribute]) -> ToolMeta {
let mut name = String::new();
let mut description = String::new();
for attr in attrs {
if !attr.path().is_ident("tool") {
continue;
}
if let Meta::List(ml) = &attr.meta {
let _ = ml.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let content = meta.value()?;
let lit: syn::LitStr = content.parse()?;
name = lit.value();
} else if meta.path.is_ident("description") {
let content = meta.value()?;
let lit: syn::LitStr = content.parse()?;
description = lit.value();
}
Ok(())
});
}
}
ToolMeta { name, description }
}
pub(crate) fn extract_doc_from_attrs(attrs: &[Attribute]) -> Option<String> {
let mut docs = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc")
&& let Meta::NameValue(nv) = &attr.meta
&& let Expr::Lit(el) = &nv.value
&& let Lit::Str(s) = &el.lit
{
for line in s.value().lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
docs.push(trimmed.to_string());
}
}
}
}
if docs.is_empty() {
None
} else {
Some(docs.join(" "))
}
}
pub(crate) struct FnParam {
pub(crate) ident: syn::Ident,
pub(crate) ty: syn::Type,
pub(crate) doc_attrs: Vec<Attribute>,
}
pub(crate) fn extract_fn_params(
inputs: &Punctuated<FnArg, syn::Token![,]>,
) -> Result<Vec<FnParam>, syn::Error> {
let mut result = Vec::new();
for arg in inputs.iter() {
match arg {
FnArg::Typed(typed) => {
let ident = match &*typed.pat {
Pat::Ident(pat_ident) => pat_ident.ident.clone(),
_ => {
return Err(syn::Error::new_spanned(
&typed.pat,
"only simple identifiers are supported as tool parameters",
));
}
};
let doc_attrs: Vec<Attribute> = typed
.attrs
.iter()
.filter(|a| a.path().is_ident("doc"))
.cloned()
.collect();
result.push(FnParam {
ident,
ty: (*typed.ty).clone(),
doc_attrs,
});
}
FnArg::Receiver(_recv) => {
return Err(syn::Error::new_spanned(
_recv,
"self parameters are not supported in #[tool] functions",
));
}
}
}
Ok(result)
}
pub(crate) fn parse_struct_meta(attrs: &[Attribute], input: &DeriveInput) -> (String, String) {
let helper_meta = extract_helper_meta(attrs);
let doc = extract_doc_from_attrs(attrs);
let name = if !helper_meta.name.is_empty() {
helper_meta.name
} else {
ident_to_snake_case(&input.ident.to_string())
};
let description = if !helper_meta.description.is_empty() {
helper_meta.description
} else {
doc.unwrap_or_default()
};
(name, description)
}
pub(crate) fn ident_to_snake_case(s: &str) -> String {
use heck::ToSnakeCase;
s.to_snake_case()
}
pub(crate) fn snake_to_pascal(s: &str) -> String {
use heck::ToPascalCase;
s.to_pascal_case()
}