allora_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse::{Parse, ParseStream};
4use syn::{parse_macro_input, ImplItem, ImplItemFn, ItemImpl};
5use syn::{punctuated::Punctuated, Ident, LitStr, Token};
6
7#[derive(Debug)]
8enum ServiceArgKind {
9    Name(String),
10}
11
12#[derive(Debug, Default)]
13struct ServiceArgs {
14    name: Option<String>,
15}
16
17impl Parse for ServiceArgs {
18    fn parse(input: ParseStream) -> syn::Result<Self> {
19        let mut args = ServiceArgs::default();
20        let punct: Punctuated<ArgItem, Token![,]> =
21            input.parse_terminated(ArgItem::parse, Token![,])?;
22        for item in punct {
23            match item.kind {
24                ServiceArgKind::Name(v) => {
25                    if args.name.is_some() {
26                        return Err(syn::Error::new(item.span, "duplicate name"));
27                    }
28                    args.name = Some(v);
29                }
30            }
31        }
32        Ok(args)
33    }
34}
35
36struct ArgItem {
37    kind: ServiceArgKind,
38    span: proc_macro2::Span,
39}
40impl Parse for ArgItem {
41    fn parse(input: ParseStream) -> syn::Result<Self> {
42        let ident: Ident = input.parse()?;
43        let span = ident.span();
44        if ident == "name" {
45            let _eq: Token![=] = input.parse()?;
46            let lit: LitStr = input.parse()?;
47            return Ok(ArgItem {
48                kind: ServiceArgKind::Name(lit.value()),
49                span,
50            });
51        }
52        Err(syn::Error::new(
53            span,
54            "unsupported #[service] argument; expected name=...",
55        ))
56    }
57}
58
59/// Attribute macro for registering a Service implementation with the runtime inventory.
60///
61/// Placement: apply to an inherent impl block that defines a constructor and (separately) implements
62/// the `Service` trait. Example:
63///
64/// ```ignore
65/// use allora::{Service, Exchange, error::Result};
66/// struct MyService;
67/// impl MyService { pub fn new() -> Self { Self } }
68/// #[service(name="my_service")]
69/// impl MyService { /* may also hold helper fns */ }
70/// impl Service for MyService { /* provide process/process_sync */ }
71/// ```
72///
73/// Supported forms:
74/// * `#[service(name = "custom")]` – explicit implementation identifier to match YAML `service_activator.ref-name`.
75/// * Combination: `#[service(name="x")]`
76///
77/// Constraints / validation:
78/// * Must be on an inherent impl (trait impls rejected).
79/// * Generics unsupported (emit compile error) – keeps inventory constructor stable.
80/// * A zero-arg `new()` method must appear in THIS impl block.
81///
82/// Reusability / Testability improvements over the prior version:
83/// * Structured parsing (no brittle string slicing).
84/// * Helpful, span-anchored compile errors.
85/// * Optional per-call instantiation for stateful / non-thread-safe services.
86/// * Clear default naming semantics.
87///
88/// NOTE: The macro intentionally does not assert that the Service trait is implemented; the user
89/// will receive a normal Rust method resolution error if `process` / `process_sync` is missing.
90#[proc_macro_attribute]
91pub fn service(attr: TokenStream, item: TokenStream) -> TokenStream {
92    let parsed_args = if attr.is_empty() {
93        ServiceArgs::default()
94    } else {
95        parse_macro_input!(attr as ServiceArgs)
96    };
97    let name_block = parse_macro_input!(item as ItemImpl);
98
99    if name_block.trait_.is_some() {
100        return syn::Error::new_spanned(
101            &name_block.self_ty,
102            "#[service] must be applied to an inherent impl (e.g. impl Type { ... })",
103        )
104        .to_compile_error()
105        .into();
106    }
107    if !name_block.generics.params.is_empty() {
108        return syn::Error::new_spanned(
109            &name_block.generics,
110            "#[service] does not currently support generic impl blocks",
111        )
112        .to_compile_error()
113        .into();
114    }
115
116    let self_ty = name_block.self_ty.clone();
117
118    let mut has_new = false;
119    // Always require zero-arg new now (no alternate ctor support).
120    for item in &name_block.items {
121        if let ImplItem::Fn(ImplItemFn { sig, .. }) = item {
122            if sig.ident == "new" && sig.inputs.is_empty() {
123                has_new = true;
124                break;
125            }
126        }
127    }
128    if !has_new {
129        return syn::Error::new_spanned(
130            &name_block.self_ty,
131            "No zero-arg `new()` found in this impl block; define one for #[service]",
132        )
133        .to_compile_error()
134        .into();
135    }
136
137    let name_tokens = if let Some(n) = &parsed_args.name {
138        quote!(#n)
139    } else {
140        let type_name = self_ty.to_token_stream().to_string().replace(' ', "");
141        quote!(#type_name)
142    };
143    let gen = quote! { #name_block inventory::submit! { allora::ServiceDescriptor { name: #name_tokens, constructor: || { use std::sync::Arc; Arc::new(#self_ty::new()) as Arc<dyn allora::Service> } } } };
144    gen.into()
145}