axor-macros 0.1.1

Core of the Axor framework: agent-based business logic with typed operations and injection.
Documentation
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;

    // Adds #[derive(Default)] if not present
    let mut struct_with_default = s.clone();
    if !has_default_derive(&s.attrs) {
        struct_with_default
            .attrs
            .push(syn::parse_quote!(#[derive(Default)]));
    }

    // generates injections calls self.<field>.from_context(...)
    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()
}

/// Macro #[agent_impl] sur un impl
/// - generates  Agent impl
/// - calls do_inject_dependencies(context) on impl generated by agent macro
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(); // skip &self
            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, // trop de paramètres
            };

            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 {
                // println!("Operation name : {}", payload.op_name_unchecked());
                match payload.op_name_unchecked() {
                    #(#match_arms,)*
                    _ => crate::InvokeResult {
                        operation: payload.name.to_string(),
                        success: false,
                        data: None,
                    }
                }
            }
        }
    };

    gen.into()
}

// Helpers

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
    }
}