use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{FnArg, Ident, PatType, Type};
pub(crate) struct Handler {
pub(crate) fn_name: Ident,
pub(crate) tool_name: String,
pub(crate) description: Option<String>,
pub(crate) takes_context: bool,
pub(crate) positional: Vec<PatType>,
pub(crate) returns_result: bool,
pub(crate) returns_tool_result: bool,
pub(crate) prefix: String,
}
impl Handler {
pub(crate) fn args_ident(&self) -> Ident {
format_ident!("__{}_{}_Args", self.prefix, self.fn_name)
}
}
pub(crate) fn last_segment(ty: &Type) -> Option<&Ident> {
match ty {
Type::Path(path) => path.path.segments.last().map(|segment| &segment.ident),
_ => None,
}
}
fn is_context(arg: &FnArg) -> bool {
let FnArg::Typed(typed) = arg else {
return false;
};
last_segment(&typed.ty).is_some_and(|ident| {
let name = ident.to_string();
name == "RequestContext" || name == "PeerSession"
})
}
pub(crate) fn read_handler(
attrs: &[syn::Attribute],
attribute: proc_macro2::TokenStream,
sig: &syn::Signature,
) -> syn::Result<Handler> {
if sig.asyncness.is_none() {
return Err(syn::Error::new_spanned(
sig,
"a #[tool] handler is awaited when a peer calls it, so it has to be an `async fn`: \
the generated factory calls it and awaits the future it returns, and a plain `fn` \
answers with a value that has nothing to await. Mark it `async`, or if it really \
does no waiting, keep the `async` — an idle future is cheaper than an unbuildable \
server.",
));
}
let fields = literal_fields(attribute)?;
let tool_name = field(&fields, "name").unwrap_or_else(|| sig.ident.to_string());
let description = field(&fields, "description").or_else(|| description_from(attrs));
let mut takes_context = false;
let mut positional = Vec::new();
let mut seen_argument = false;
for arg in &sig.inputs {
match arg {
FnArg::Receiver(receiver) => {
return Err(syn::Error::new_spanned(
receiver,
"a #[tool] handler takes no `self`: a tool is called by name, so a \
receiver would have nothing to receive from. Share state through \
`Server::builder().with_state(value)` and read it from the context: \
`ctx.state::<MyType>()`.",
));
}
FnArg::Typed(typed) if is_context(arg) => {
if takes_context {
return Err(syn::Error::new_spanned(
typed,
"a #[tool] handler takes at most one context parameter, and this \
is the second",
));
}
if seen_argument {
return Err(syn::Error::new_spanned(
typed,
"a #[tool] handler takes its context first, before the arguments: \
the generated handler is given the context and then the decoded \
arguments, in that order, so a context written after an argument \
has nowhere to go. Move it to the front of the parameter list.",
));
}
takes_context = true;
}
FnArg::Typed(typed) => {
seen_argument = true;
positional.push(typed.clone());
}
}
}
Ok(Handler {
fn_name: sig.ident.clone(),
tool_name,
description,
takes_context,
positional,
returns_result: returns_result(sig),
returns_tool_result: returns_tool_result(sig),
prefix: String::new(),
})
}
pub(crate) fn args_struct(handler: &Handler) -> TokenStream {
let args_ident = handler.args_ident();
if handler.positional.is_empty() {
return quote! {
#[derive(
::protocol_droid::schemars::JsonSchema,
::protocol_droid::serde::Deserialize,
::core::default::Default,
)]
#[serde(crate = "::protocol_droid::serde")]
#[allow(
dead_code,
missing_docs,
non_camel_case_types,
reason = "generated by #[server], and never named by a user",
)]
struct #args_ident {}
};
}
let members = handler.positional.iter().map(|field| {
let PatType { pat, ty, .. } = field;
quote! { #pat: #ty }
});
quote! {
#[derive(
::protocol_droid::schemars::JsonSchema,
::protocol_droid::serde::Deserialize,
)]
#[serde(crate = "::protocol_droid::serde")]
#[allow(
dead_code,
missing_docs,
non_camel_case_types,
reason = "generated by #[server], and never named by a user",
)]
struct #args_ident {
#(#members,)*
}
}
}
pub(crate) fn tool_fn_ident(handler: &Handler) -> proc_macro2::Ident {
format_ident!("__{}_{}_TOOL", handler.prefix, handler.fn_name)
}
pub(crate) fn handler_fn_ident(handler: &Handler) -> proc_macro2::Ident {
format_ident!("__{}_{}_HANDLER", handler.prefix, handler.fn_name)
}
pub(crate) fn tool_fn(handler: &Handler) -> TokenStream {
let tool_fn = format_ident!("__{}_{}_TOOL", handler.prefix, handler.fn_name);
let args_ident = handler.args_ident();
let tool_name = &handler.tool_name;
let described = handler.description.as_ref().map_or_else(
proc_macro2::TokenStream::new,
|text| quote! { .described(#text) },
);
quote! {
#[allow(
dead_code,
missing_docs,
non_snake_case,
reason = "generated by #[server], and never named by a user",
)]
fn #tool_fn() -> ::core::result::Result<
::protocol_droid::domain::registry::Tool,
::protocol_droid::domain::ir::JsonObjectError,
> {
let schema = ::protocol_droid::schemars::schema_for!(#args_ident);
let text = ::protocol_droid::serde_json::to_string(&schema)
.map_err(|_| ::protocol_droid::domain::ir::JsonObjectError::NotAnObject)?;
Ok(
::protocol_droid::domain::registry::Tool::new(#tool_name, &text)?
#described,
)
}
}
}
fn outcome(handler: &Handler, call_args: &TokenStream) -> TokenStream {
if handler.returns_tool_result {
if is_result(handler) {
return quote! {
match function(#call_args).await {
Ok(answer) => {
::protocol_droid::adapters::secondary::codec::result::verbatim(
context.peer().negotiated(),
&answer,
)
}
Err(error) => {
Ok(::protocol_droid::application::server::result::failure(error))
}
}
};
}
return quote! {
::protocol_droid::adapters::secondary::codec::result::verbatim(
context.peer().negotiated(),
&function(#call_args).await,
)
};
}
if is_result(handler) {
quote! {
match function(#call_args).await {
Ok(answer) => ::protocol_droid::application::server::result::success(&answer),
Err(error) => {
Ok(::protocol_droid::application::server::result::failure(error))
}
}
}
} else {
quote! {
::protocol_droid::application::server::result::success(
&function(#call_args).await
)
}
}
}
fn generics(
handler: &Handler,
f_ty: &Ident,
fut_ty: &Ident,
error_ty: &Ident,
answer_ty: &Ident,
) -> TokenStream {
match (is_result(handler), handler.returns_tool_result) {
(true, false) => quote! { <#f_ty, #fut_ty, #error_ty, #answer_ty> },
(true, true) => quote! { <#f_ty, #fut_ty, #error_ty> },
(false, false) => quote! { <#f_ty, #fut_ty, #answer_ty> },
(false, true) => quote! { <#f_ty, #fut_ty> },
}
}
fn bounds(handler: &Handler, fut_ty: &Ident, error_ty: &Ident, answer_ty: &Ident) -> TokenStream {
let concrete = quote! { ::protocol_droid::domain::ir::ToolResult };
let answer_ty = if handler.returns_tool_result {
concrete
} else {
quote! { #answer_ty }
};
let answer_bound = if handler.returns_tool_result {
quote! {}
} else {
quote! { , #answer_ty: ::protocol_droid::serde::Serialize }
};
if is_result(handler) {
quote! {
#fut_ty: ::core::future::Future<Output = ::core::result::Result<#answer_ty, #error_ty>>
+ ::core::marker::Send,
#error_ty: ::core::fmt::Display
#answer_bound
}
} else {
quote! {
#fut_ty: ::core::future::Future<Output = #answer_ty> + ::core::marker::Send
#answer_bound
}
}
}
pub(crate) fn handler_factory(handler: &Handler) -> TokenStream {
let args_ident = handler.args_ident();
let f_ty = format_ident!("__F");
let fut_ty = format_ident!("__Fut");
let error_ty = format_ident!("__Error");
let answer_ty = format_ident!("__Answer");
let plain: Vec<&syn::PatType> = handler.positional.iter().collect();
let field_names: Vec<&syn::Ident> = plain
.iter()
.filter_map(|field| match &*field.pat {
syn::Pat::Ident(ident) => Some(&ident.ident),
_ => None,
})
.collect();
let field_types: Vec<&syn::Type> = plain.iter().map(|field| field.ty.as_ref()).collect();
let call_args = if handler.takes_context {
quote! { context.clone(), #(#field_names),* }
} else {
quote! { #(#field_names),* }
};
let binding = if field_names.is_empty() {
quote! { #args_ident }
} else {
quote! { #args_ident { #(#field_names),* } }
};
let (fn_inputs, use_context) = if handler.takes_context {
let mut inputs = vec![quote! { ::protocol_droid::application::server::RequestContext }];
inputs.extend(field_types.iter().map(|ty| quote! { #ty }));
(inputs, quote! {})
} else {
let inputs = field_types
.iter()
.map(|ty| quote! { #ty })
.collect::<Vec<_>>();
(inputs, quote! { let _ = &context; })
};
let handler_fn = format_ident!("__{}_{}_HANDLER", handler.prefix, handler.fn_name);
let generics_clause = generics(handler, &f_ty, &fut_ty, &error_ty, &answer_ty);
let bounds_clause = bounds(handler, &fut_ty, &error_ty, &answer_ty);
let outcome = outcome(handler, &call_args);
quote! {
#[allow(
dead_code,
missing_docs,
non_snake_case,
reason = "generated by #[server], and never named by a user",
)]
fn #handler_fn #generics_clause (f: #f_ty) -> ::protocol_droid::application::server::ToolHandler
where
#f_ty: ::core::ops::Fn(#(#fn_inputs),*) -> #fut_ty
+ ::core::marker::Send
+ ::core::marker::Sync
+ 'static,
#bounds_clause,
{
let function = ::std::sync::Arc::new(f);
::std::sync::Arc::new(move |context, arguments| {
let function = ::std::sync::Arc::clone(&function);
Box::pin(async move {
let #binding: #args_ident = match ::protocol_droid::serde_json::from_str(
arguments.as_str()
) {
Ok(decoded) => decoded,
Err(error) => {
return
::protocol_droid::application::server::result::failure(
::protocol_droid::application::server::result::OutcomeError::Arguments(
error.to_string(),
),
);
}
};
#use_context
match (#outcome) {
Ok(document) => document,
Err(error) => {
::protocol_droid::application::server::result::failure(error)
}
}
})
})
}
}
}
pub(crate) const fn is_result(handler: &Handler) -> bool {
handler.returns_result
}
pub(crate) fn returns_result(sig: &syn::Signature) -> bool {
match &sig.output {
syn::ReturnType::Default => false,
syn::ReturnType::Type(_, ty) => last_segment(ty).is_some_and(|ident| ident == "Result"),
}
}
pub(crate) fn returns_tool_result(sig: &syn::Signature) -> bool {
let syn::ReturnType::Type(_, ty) = &sig.output else {
return false;
};
let ty = ty.as_ref();
if last_segment(ty).is_some_and(|ident| ident == "ToolResult") {
return true;
}
let Type::Path(path) = ty else {
return false;
};
let Some(segment) = path.path.segments.last() else {
return false;
};
if segment.ident != "Result" {
return false;
}
let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return false;
};
arguments.args.iter().any(|argument| match argument {
syn::GenericArgument::Type(inner) => {
last_segment(inner).is_some_and(|ident| ident == "ToolResult")
}
_ => false,
})
}
pub(crate) fn literal_fields(
attribute: proc_macro2::TokenStream,
) -> syn::Result<Vec<(String, syn::LitStr)>> {
if attribute.is_empty() {
return Ok(Vec::new());
}
let nested = syn::parse::Parser::parse2(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
attribute,
)?;
let mut found = Vec::new();
for entry in nested {
let syn::Meta::NameValue(pair) = entry else {
return Err(syn::Error::new_spanned(
&entry,
"expected a named value, for example name = \"my-tool\"",
));
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(text),
..
}) = &pair.value
else {
return Err(syn::Error::new_spanned(
&pair.value,
"expected a string literal, for example name = \"my-tool\"",
));
};
found.push((
pair.path
.get_ident()
.map_or_else(String::new, ToString::to_string),
text.clone(),
));
}
Ok(found)
}
pub(crate) fn field(fields: &[(String, syn::LitStr)], name: &str) -> Option<String> {
fields
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.value())
}
pub(crate) fn description_from(attrs: &[syn::Attribute]) -> Option<String> {
let mut lines: Vec<String> = Vec::new();
for attr in attrs {
if !attr.path().is_ident("doc") {
continue;
}
let syn::Meta::NameValue(pair) = &attr.meta else {
continue;
};
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(text),
..
}) = &pair.value
else {
continue;
};
let line = text.value();
if line.trim().is_empty() && !lines.is_empty() {
break;
}
if !line.trim().is_empty() {
lines.push(line.trim().to_string());
}
}
if lines.is_empty() {
None
} else {
Some(lines.join(" "))
}
}