use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, Attribute, Fields, FnArg, ItemImpl, ItemStruct, PatType, ReturnType, Type,
TypePath,
};
pub fn mark_agent_struct(input: TokenStream) -> TokenStream {
let s = parse_macro_input!(input as ItemStruct);
let struct_ident = &s.ident;
let mut struct_with_default = s.clone();
if !has_default_derive(&s.attrs) {
struct_with_default
.attrs
.push(syn::parse_quote!(#[derive(Default)]));
}
let injections = if let Fields::Named(fields_named) = &s.fields {
fields_named
.named
.iter()
.filter_map(|field| {
let ident = field.ident.as_ref()?;
if is_inject_type(&field.ty) {
Some(quote! { self.#ident.from_context(context); })
} else {
None
}
})
.collect::<Vec<_>>()
} else {
vec![]
};
let gen = quote! {
#struct_with_default
impl #struct_ident {
pub fn do_inject_dependencies(&self, context: &crate::AxorContext) {
#(#injections)*
}
}
};
gen.into()
}
pub fn expand_agent_impl(input: TokenStream) -> TokenStream {
let item_impl = parse_macro_input!(input as ItemImpl);
let self_ty = &item_impl.self_ty;
let struct_ident = if let Type::Path(TypePath { path, .. }) = &**self_ty {
path.get_ident().expect("Expected a struct").clone()
} else {
panic!("#[agent_impl] must be used on an impl of a struct");
};
let mut match_arms = Vec::new();
let mut op_names = Vec::new();
for item in &item_impl.items {
if let syn::ImplItem::Fn(method) = item {
if !has_operation_attr(&method.attrs) {
continue;
}
let ident = &method.sig.ident;
let op_name = ident.to_string();
op_names.push(op_name.clone());
let inputs: Vec<_> = method.sig.inputs.iter().skip(1).collect(); eprintln!(
"→ Found #[operation] method: {} (args: {})",
ident,
inputs.len()
);
let (arg_decl, call_expr) = match inputs.len() {
0 => (quote! {}, quote! { self.#ident() }),
1 => {
let ty = match &inputs[0] {
FnArg::Typed(PatType { ty, .. }) => ty,
_ => continue,
};
(
quote! {
let arg0: #ty = match payload.input_as() {
Some(val) => val,
None => return crate::InvokeResult {
operation: #op_name.into(),
success: false,
data: None,
}
};
},
quote! { self.#ident(arg0) },
)
}
_ => continue, };
let has_return = !matches!(method.sig.output, ReturnType::Default);
let match_arm = if has_return {
quote! {
#op_name => {
#arg_decl
let result = #call_expr;
let data = serde_json::to_value(result).ok();
crate::InvokeResult {
operation: #op_name.into(),
success: true,
data,
}
}
}
} else {
quote! {
#op_name => {
#arg_decl
#call_expr;
crate::InvokeResult {
operation: #op_name.into(),
success: true,
data: None,
}
}
}
};
match_arms.push(match_arm);
}
}
let gen = quote! {
#item_impl
impl crate::Agent for #struct_ident {
fn name(&self) -> &'static str {
stringify!(#struct_ident)
}
fn operations(&self) -> Vec<crate::OperationDescriptor> {
vec![
#( crate::OperationDescriptor { name: #op_names } ),*
]
}
fn inject_dependencies(&self, context: &crate::AxorContext) {
self.do_inject_dependencies(context);
}
fn call_operation(&self, payload: &crate::Payload) -> crate::InvokeResult {
match payload.op_name_unchecked() {
#(#match_arms,)*
_ => crate::InvokeResult {
operation: payload.name.to_string(),
success: false,
data: None,
}
}
}
}
};
gen.into()
}
fn has_default_derive(attrs: &[Attribute]) -> bool {
for attr in attrs {
if attr.path().is_ident("derive") {
let Ok(meta_list) = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
) else {
continue;
};
if meta_list.iter().any(|p| p.is_ident("Default")) {
return true;
}
}
}
false
}
fn has_operation_attr(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| attr.path().is_ident("operation"))
}
fn is_inject_type(ty: &Type) -> bool {
if let Type::Path(TypePath { path, .. }) = ty {
path.segments
.first()
.map(|seg| seg.ident == "Inject")
.unwrap_or(false)
} else {
false
}
}