Skip to main content

blvm_sdk_macros/
lib.rs

1//! Procedural macros for BLVM SDK CLI module system.
2
3mod cli_spec;
4mod event_payload_map;
5mod module_impl;
6mod rpc_methods;
7
8use proc_macro::TokenStream;
9use quote::{quote, ToTokens};
10use std::collections::HashMap;
11use syn::{
12    parse_macro_input, punctuated::Punctuated, token::Comma, DeriveInput, Field, ImplItem, Item,
13    ItemImpl, LitStr, Meta,
14};
15
16type EventToMethodsMap = HashMap<String, Vec<(syn::Ident, Vec<(String, bool)>, Vec<String>)>>;
17
18/// Attribute macro: `#[command(name = "...")]` or `#[module_cli(name = "...")]` - on struct: marks CLI entry point.
19/// On impl block: generates `cli_spec()`. Same attribute works for both.
20#[proc_macro_attribute]
21pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
22    let item = parse_macro_input!(item as Item);
23    match &item {
24        Item::Struct(_) => expand_module_cli(attr, item),
25        Item::Impl(impl_item) => expand_cli_subcommand(attr, impl_item.clone()),
26        _ => TokenStream::from(quote! { #item }),
27    }
28}
29
30/// Attribute macro: `#[module_cli(name = "...")]` - marks a struct as the module CLI entry point.
31///
32/// The `name` is the CLI command name (e.g. "sync-policy"). Emits `CLI_NAME` const.
33#[proc_macro_attribute]
34pub fn module_cli(attr: TokenStream, item: TokenStream) -> TokenStream {
35    let item = parse_macro_input!(item as Item);
36    expand_module_cli(attr, item)
37}
38
39fn expand_module_cli(attr: TokenStream, item: Item) -> TokenStream {
40    let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
41    let name = extract_name_from_meta(&args).unwrap_or_else(|| "cli".to_string());
42
43    let (struct_name, item_tokens) = match &item {
44        Item::Struct(s) => (s.ident.clone(), quote! { #item }),
45        _ => return TokenStream::from(quote! { #item }),
46    };
47
48    let name_lit = LitStr::new(&name, proc_macro2::Span::call_site());
49    let impl_block = quote! {
50        impl #struct_name {
51            /// CLI command name (from #[command(name = "...")]).
52            pub const CLI_NAME: &str = #name_lit;
53        }
54    };
55
56    TokenStream::from(quote! {
57        #item_tokens
58        #impl_block
59    })
60}
61
62/// Attribute macro: `#[cli_subcommand]` or `#[cli_subcommand(name = "...")]` - on impl block.
63///
64/// Generates `cli_spec()` function returning `blvm_node::module::ipc::protocol::CliSpec`.
65#[proc_macro_attribute]
66pub fn cli_subcommand(attr: TokenStream, item: TokenStream) -> TokenStream {
67    let item = parse_macro_input!(item as ItemImpl);
68    expand_cli_subcommand(attr, item)
69}
70
71fn expand_cli_subcommand(attr: TokenStream, mut item: ItemImpl) -> TokenStream {
72    let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
73    let cli_name = extract_name_from_meta(&args);
74
75    let derived_name = match &item.self_ty.as_ref() {
76        syn::Type::Path(p) => {
77            let ty_name = p
78                .path
79                .segments
80                .last()
81                .map(|s| s.ident.to_string())
82                .unwrap_or_default();
83            if ty_name.ends_with("Cli") {
84                let base = &ty_name[..ty_name.len() - 3];
85                base.chars()
86                    .enumerate()
87                    .flat_map(|(i, c)| {
88                        if c == '_' {
89                            vec!['-']
90                        } else if c.is_uppercase() && i > 0 {
91                            vec!['-', c.to_lowercase().next().unwrap()]
92                        } else if c.is_uppercase() {
93                            vec![c.to_lowercase().next().unwrap()]
94                        } else {
95                            vec![c]
96                        }
97                    })
98                    .collect::<String>()
99            } else {
100                ty_name.to_lowercase().replace('_', "-")
101            }
102        }
103        _ => "cli".to_string(),
104    };
105
106    let cli_name = cli_name.unwrap_or(derived_name);
107
108    let spec_code = cli_spec::generate_spec_code(&item, &cli_name);
109
110    let fn_item: ImplItem = syn::parse2(quote! {
111        /// Generated CLI spec for module registration.
112        pub fn cli_spec() -> blvm_node::module::ipc::protocol::CliSpec {
113            #spec_code
114        }
115    })
116    .expect("generated fn should parse");
117
118    item.items.push(fn_item);
119
120    if let Some(dispatch_code) = cli_spec::generate_dispatch_cli(&item) {
121        let dispatch_item: ImplItem =
122            syn::parse2(dispatch_code).expect("dispatch_cli should parse");
123        item.items.push(dispatch_item);
124    }
125
126    TokenStream::from(quote! { #item })
127}
128
129pub(crate) fn extract_name_from_meta(args: &Punctuated<Meta, Comma>) -> Option<String> {
130    for meta in args {
131        if let Meta::NameValue(nv) = meta {
132            if nv.path.is_ident("name") {
133                if let syn::Expr::Lit(el) = &nv.value {
134                    if let syn::Lit::Str(s) = &el.lit {
135                        return Some(s.value());
136                    }
137                }
138            }
139        }
140    }
141    None
142}
143
144/// Extract config type from #[module(config = DemoConfig)].
145pub(crate) fn extract_config_type_from_meta(args: &Punctuated<Meta, Comma>) -> Option<syn::Type> {
146    for meta in args {
147        if let Meta::NameValue(nv) = meta {
148            if nv.path.is_ident("config") {
149                return syn::parse2(nv.value.to_token_stream()).ok();
150            }
151        }
152    }
153    None
154}
155
156/// Infer config type from struct field `config: T`.
157fn extract_config_type_from_struct(struct_item: &syn::ItemStruct) -> Option<syn::Type> {
158    if let syn::Fields::Named(ref fields) = struct_item.fields {
159        for f in &fields.named {
160            if f.ident.as_ref().map(|i| i == "config").unwrap_or(false) {
161                return Some(f.ty.clone());
162            }
163        }
164    }
165    None
166}
167
168/// Derive module name from struct/type name: DemoModule → "demo", SyncPolicy → "sync-policy".
169pub(crate) fn derive_module_name(ident: &syn::Ident) -> String {
170    let s = ident.to_string();
171    let s = s.strip_suffix("Module").unwrap_or(&s);
172    s.chars()
173        .enumerate()
174        .flat_map(|(i, c)| {
175            if c == '_' {
176                vec!['-']
177            } else if c.is_uppercase() && i > 0 {
178                vec!['-', c.to_lowercase().next().unwrap()]
179            } else if c.is_uppercase() {
180                vec![c.to_lowercase().next().unwrap()]
181            } else {
182                vec![c]
183            }
184        })
185        .collect::<String>()
186}
187
188/// Extract migrations from #[module(migrations = ((1, up_initial), (2, up_add_items_tree)))].
189/// Returns Vec of (version_lit, fn_ident) for generating the migrations slice.
190fn extract_migrations_from_meta(args: &Punctuated<Meta, Comma>) -> Option<Vec<(u32, syn::Ident)>> {
191    for meta in args {
192        if let Meta::NameValue(nv) = meta {
193            if !nv.path.is_ident("migrations") {
194                continue;
195            }
196            let expr = &nv.value;
197            let mut pairs = Vec::new();
198            if let syn::Expr::Tuple(outer) = expr {
199                for elem in &outer.elems {
200                    if let syn::Expr::Tuple(inner) = elem {
201                        let elems: Vec<_> = inner.elems.iter().collect();
202                        if elems.len() >= 2 {
203                            let version = match &elems[0] {
204                                syn::Expr::Lit(el) => {
205                                    if let syn::Lit::Int(li) = &el.lit {
206                                        li.base10_parse::<u32>().ok()
207                                    } else {
208                                        None
209                                    }
210                                }
211                                _ => None,
212                            };
213                            let ident = match &elems[1] {
214                                syn::Expr::Path(ep) => ep.path.get_ident().cloned(),
215                                _ => None,
216                            };
217                            if let (Some(v), Some(i)) = (version, ident) {
218                                pairs.push((v, i));
219                            }
220                        }
221                    }
222                }
223            }
224            if !pairs.is_empty() {
225                return Some(pairs);
226            }
227        }
228    }
229    None
230}
231
232/// Attribute macro: `#[rpc_methods]` - on impl block with #[rpc_method] methods.
233///
234/// Generates `rpc_method_names() -> Vec<&'static str>` and `dispatch_rpc(&self, method, params, db)`.
235/// Enables auto-discovery of RPC methods for run_module!.
236#[proc_macro_attribute]
237pub fn rpc_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
238    let item = parse_macro_input!(item as ItemImpl);
239    rpc_methods::expand_rpc_methods(item).into()
240}
241
242/// Attribute macro: `#[rpc_method]` or `#[rpc_method(name = "...")]`.
243///
244/// Marks a method as an RPC endpoint. Without `name`, the function name is used (e.g. `demo_set` → "demo_set").
245/// Use with #[rpc_methods] or #[module(name = "x")] on the impl block for auto-discovery.
246#[proc_macro_attribute]
247pub fn rpc_method(_attr: TokenStream, item: TokenStream) -> TokenStream {
248    let item = parse_macro_input!(item as ImplItem);
249    TokenStream::from(quote! { #item })
250}
251
252/// Attribute macro: `#[migration(version = N)]` or `#[migration(version = N, down)]`.
253///
254/// Marks a function as a migration step. For `up` migrations (default), the function
255/// must have signature `fn(&MigrationContext) -> Result<()>`. Use with `run_migrations`:
256///
257/// ```ignore
258/// run_migrations(&db, &[(1, up_initial), (2, up_add_cache)])?;
259/// ```
260///
261/// `down` migrations are for future rollback support; currently pass-through only.
262#[proc_macro_attribute]
263pub fn migration(attr: TokenStream, item: TokenStream) -> TokenStream {
264    let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
265    let item = parse_macro_input!(item as syn::ItemFn);
266
267    let mut version = None;
268    let mut is_down = false;
269
270    for meta in args {
271        match meta {
272            Meta::NameValue(nv) if nv.path.is_ident("version") => {
273                if let syn::Expr::Lit(el) = &nv.value {
274                    if let syn::Lit::Int(li) = &el.lit {
275                        version = li.base10_parse::<u32>().ok();
276                    }
277                }
278            }
279            Meta::Path(p) if p.is_ident("down") => is_down = true,
280            _ => {}
281        }
282    }
283
284    if version.is_none() {
285        return syn::Error::new(
286            proc_macro2::Span::call_site(),
287            "#[migration] requires version = N (e.g. #[migration(version = 1)])",
288        )
289        .to_compile_error()
290        .into();
291    }
292
293    let _version = version.unwrap();
294    let _is_down = is_down;
295    // Pass through; module author collects migrations and passes to run_migrations
296    TokenStream::from(quote! { #item })
297}
298
299/// Extract env var name from `#[config_env]` or `#[config_env("ENV_NAME")]` on a field.
300/// Returns None if no config_env attr; Some(env_name) if present (None = use default).
301fn extract_config_env_from_field(field: &Field) -> Option<Option<String>> {
302    for attr in &field.attrs {
303        if attr.path().is_ident("config_env") {
304            // #[config_env] with no args is Meta::Path; parse_args fails on empty.
305            // #[config_env("X")] is Meta::List. Use meta structure directly.
306            return Some(match &attr.meta {
307                Meta::Path(_) => None, // #[config_env] - use default MODULE_CONFIG_<FIELD>
308                Meta::NameValue(nv) if nv.path.is_ident("env") => {
309                    if let syn::Expr::Lit(el) = &nv.value {
310                        if let syn::Lit::Str(s) = &el.lit {
311                            Some(s.value())
312                        } else {
313                            None
314                        }
315                    } else {
316                        None
317                    }
318                }
319                Meta::List(list) => syn::parse2::<LitStr>(list.tokens.clone())
320                    .ok()
321                    .map(|s| s.value()),
322                _ => None,
323            });
324        }
325    }
326    None
327}
328
329/// Generate env override assignment for a field based on its type.
330fn env_override_stmt(
331    field_name: &syn::Ident,
332    env_lit: &LitStr,
333    ty: &syn::Type,
334) -> proc_macro2::TokenStream {
335    let ty_str = ty.to_token_stream().to_string();
336    let ty_compact = ty_str.replace(' ', "");
337    if ty_compact == "String" {
338        quote! {
339            if let Ok(__v) = std::env::var(#env_lit) {
340                self.#field_name = __v;
341            }
342        }
343    } else if ty_compact.starts_with("Option<") {
344        quote! {
345            if let Ok(__v) = std::env::var(#env_lit) {
346                if let Ok(__parsed) = __v.parse() {
347                    self.#field_name = Some(__parsed);
348                }
349            }
350        }
351    } else if ty_compact.starts_with("Vec<") {
352        quote! {
353            if let Ok(__v) = std::env::var(#env_lit) {
354                self.#field_name = __v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
355            }
356        }
357    } else if ty_compact == "bool" {
358        quote! {
359            if let Ok(__v) = std::env::var(#env_lit) {
360                self.#field_name = __v.eq_ignore_ascii_case("true") || __v == "1";
361            }
362        }
363    } else {
364        quote! {
365            if let Ok(__v) = std::env::var(#env_lit) {
366                if let Ok(__parsed) = __v.parse() {
367                    self.#field_name = __parsed;
368                }
369            }
370        }
371    }
372}
373
374/// Attribute macro: `#[config(name = "...")]` or `#[module_config(name = "...")]` - marks a config struct.
375///
376/// The `name` matches the node config section `[modules.<name>]` for override merging.
377/// Emits `CONFIG_SECTION_NAME` const. Config loading (from env vars or file) is module-specific.
378///
379/// Field-level `#[config_env]` or `#[config_env("ENV_NAME")]`: env override for that field.
380/// - `#[config_env]` → uses `MODULE_CONFIG_<FIELD_UPPERCASE>` (node passes these)
381/// - `#[config_env("CUSTOM_ENV")]` → uses `CUSTOM_ENV` for standalone/override
382///
383/// Generates `apply_env_overrides(&mut self)` to apply env vars over loaded config.
384#[proc_macro_attribute]
385pub fn config(attr: TokenStream, item: TokenStream) -> TokenStream {
386    module_config(attr, item)
387}
388
389/// Strip #[config_env] from struct fields so derive macros (Serialize, etc.) don't see it.
390fn strip_config_env_from_item(item: &Item) -> Item {
391    let Item::Struct(mut s) = item.clone() else {
392        return item.clone();
393    };
394    if let syn::Fields::Named(ref mut fields) = s.fields {
395        for field in &mut fields.named {
396            field.attrs.retain(|a| !a.path().is_ident("config_env"));
397        }
398    }
399    Item::Struct(s)
400}
401
402#[proc_macro_attribute]
403pub fn module_config(attr: TokenStream, item: TokenStream) -> TokenStream {
404    let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
405    let item = parse_macro_input!(item as Item);
406
407    let name = extract_name_from_meta(&args);
408
409    // Output struct without #[config_env] so derive macros don't error on it
410    let item_stripped = strip_config_env_from_item(&item);
411    let item_tokens = quote! { #item_stripped };
412
413    let extra = if let Some(config_name) = name {
414        let name_lit = LitStr::new(&config_name, proc_macro2::Span::call_site());
415        if let Item::Struct(s) = &item {
416            let struct_name = &s.ident;
417            let mut apply_stmts = Vec::new();
418            if let syn::Fields::Named(fields) = &s.fields {
419                for field in &fields.named {
420                    let field_name = field.ident.as_ref().expect("named field");
421                    let Some(env_opt) = extract_config_env_from_field(field) else {
422                        continue;
423                    };
424                    let env_var = env_opt.unwrap_or_else(|| {
425                        format!(
426                            "MODULE_CONFIG_{}",
427                            field_name.to_string().to_uppercase().replace('-', "_")
428                        )
429                    });
430                    let env_lit = LitStr::new(&env_var, proc_macro2::Span::call_site());
431                    let ty = &field.ty;
432                    let set_stmt = env_override_stmt(field_name, &env_lit, ty);
433                    apply_stmts.push(set_stmt);
434                }
435            }
436            let apply_block = if apply_stmts.is_empty() {
437                quote! {
438                    /// Apply env overrides to config. No #[config_env] fields; no-op.
439                    pub fn apply_env_overrides(&mut self) {}
440                }
441            } else {
442                quote! {
443                    /// Apply env overrides for fields marked with #[config_env].
444                    /// Call after loading from file; env vars override file values.
445                    pub fn apply_env_overrides(&mut self) {
446                        #(#apply_stmts)*
447                    }
448                }
449            };
450
451            let load_block = quote! {
452                /// Load config from path (e.g. config.toml), apply env overrides.
453                /// Requires `#[derive(Default, Serialize, Deserialize)]` on the struct.
454                pub fn load(path: impl std::convert::AsRef<std::path::Path>) -> std::result::Result<Self, anyhow::Error> {
455                    let mut config: Self = std::fs::read_to_string(path.as_ref())
456                        .ok()
457                        .and_then(|s| toml::from_str(&s).ok())
458                        .unwrap_or_else(|| Self::default());
459                    config.apply_env_overrides();
460                    Ok(config)
461                }
462            };
463
464            quote! {
465                impl #struct_name {
466                    /// Config section name (from #[module_config(name = "...")]).
467                    /// Matches [modules.<name>] in node config for override merging.
468                    pub const CONFIG_SECTION_NAME: &str = #name_lit;
469                    #apply_block
470                    #load_block
471                }
472            }
473        } else {
474            quote! {}
475        }
476    } else {
477        quote! {}
478    };
479
480    TokenStream::from(quote! {
481        #item_tokens
482        #extra
483    })
484}
485
486/// Attribute macro: `#[module]` or `#[blvm_module]` - marks the main module struct.
487/// With `#[module(name = "demo")]` on an impl block: generates CLI, RPC, and event
488/// dispatch from a single impl (replaces #[command], #[rpc_methods], #[event_handlers]).
489#[proc_macro_attribute]
490pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
491    blvm_module(attr, item)
492}
493
494/// Attribute macro: `#[blvm_module]` - marks the main module struct.
495/// On struct: `#[module(name = "demo", config = DemoConfig)]` generates `__module_new(config)`.
496/// With `migrations = ((1, up_initial), (2, up_add_items_tree))` also generates `ModuleMeta` impl
497/// for `run_module_main!(DemoModule)`.
498/// On impl: `#[module(name = "demo")]` generates cli_spec, dispatch_cli, rpc_method_names, etc.
499#[proc_macro_attribute]
500pub fn blvm_module(attr: TokenStream, item: TokenStream) -> TokenStream {
501    let item = parse_macro_input!(item as Item);
502    let args = parse_macro_input!(attr with Punctuated::<Meta, Comma>::parse_terminated);
503    match &item {
504        Item::Struct(struct_item) => {
505            let struct_name = &struct_item.ident;
506            let config_ty = extract_config_type_from_meta(&args)
507                .or_else(|| extract_config_type_from_struct(struct_item));
508            let migrations = extract_migrations_from_meta(&args);
509            let module_name =
510                extract_name_from_meta(&args).unwrap_or_else(|| derive_module_name(struct_name));
511
512            let mut blocks = Vec::new();
513            let ct = config_ty.as_ref();
514
515            if let (Some(ct), Some(migs)) = (ct, migrations) {
516                let name_lit = LitStr::new(&module_name, proc_macro2::Span::call_site());
517                let migration_entries: Vec<_> = migs
518                    .iter()
519                    .map(|(v, ident)| quote! { (#v, #ident as blvm_sdk::module::MigrationUp) })
520                    .collect();
521                let meta_impl = quote! {
522                    impl blvm_sdk::module::ModuleMeta for #struct_name {
523                        const MODULE_NAME: &'static str = #name_lit;
524                        type Config = #ct;
525                        fn migrations() -> &'static [(u32, blvm_sdk::module::MigrationUp)] {
526                            static MIGRATIONS: &[(u32, blvm_sdk::module::MigrationUp)] = &[#(#migration_entries),*];
527                            MIGRATIONS
528                        }
529                        fn __module_new(config: Self::Config) -> Self {
530                            Self { config }
531                        }
532                    }
533                };
534                blocks.push(meta_impl);
535            } else if let Some(ct) = ct {
536                let impl_block = quote! {
537                    impl #struct_name {
538                        #[doc(hidden)]
539                        pub fn __module_new(config: #ct) -> Self {
540                            Self { config }
541                        }
542                    }
543                };
544                blocks.push(impl_block);
545            }
546
547            if blocks.is_empty() {
548                TokenStream::from(quote! { #item })
549            } else {
550                TokenStream::from(quote! {
551                    #item
552                    #(#blocks)*
553                })
554            }
555        }
556        Item::Impl(impl_item) => module_impl::expand_module_impl(&args, impl_item.clone()),
557        _ => TokenStream::from(quote! { #item }),
558    }
559}
560
561/// Attribute macro: `#[on_event(NewBlock, NewTransaction, ...)]` - marks a method as event handler.
562/// Use with `#[event_handlers]` on the impl block. Pass-through; event_handlers reads this.
563#[proc_macro_attribute]
564pub fn on_event(_attr: TokenStream, item: TokenStream) -> TokenStream {
565    item
566}
567
568/// Attribute macro: `#[event_handlers]` - on impl block with #[on_event] methods.
569///
570/// Generates:
571/// - `event_types() -> Vec<EventType>` — all event types from #[on_event] handlers (for subscribe_events)
572/// - `dispatch_event(&self, event: EventMessage) -> impl Future` — dispatches to the right handler
573///
574/// Auto-subscribe: call `integration.subscribe_events(MyModule::event_types()).await?` after connect.
575/// Auto-unsubscribe: happens when module unloads (IPC connection closes).
576#[proc_macro_attribute]
577pub fn event_handlers(_attr: TokenStream, item: TokenStream) -> TokenStream {
578    let mut impl_block = parse_macro_input!(item as ItemImpl);
579
580    // Collect (event_type_ident, (method_ident, params, event_types_for_method)) from each #[on_event] method
581    let mut event_to_methods: EventToMethodsMap = HashMap::new();
582    let mut all_event_idents = Vec::<syn::Ident>::new();
583
584    for impl_item in &impl_block.items {
585        if let ImplItem::Fn(method) = impl_item {
586            for attr in &method.attrs {
587                if attr.path().is_ident("on_event") {
588                    let event_idents = parse_on_event_args(attr);
589                    let method_ident = method.sig.ident.clone();
590                    let params = parse_handler_params(method);
591                    let event_keys: Vec<String> =
592                        event_idents.iter().map(|e| e.to_string()).collect();
593                    for ev in &event_idents {
594                        let key = ev.to_string();
595                        if !all_event_idents.iter().any(|e| e == ev) {
596                            all_event_idents.push(ev.clone());
597                        }
598                        event_to_methods.entry(key).or_default().push((
599                            method_ident.clone(),
600                            params.clone(),
601                            event_keys.clone(),
602                        ));
603                    }
604                    break;
605                }
606            }
607        }
608    }
609
610    if all_event_idents.is_empty() {
611        return TokenStream::from(quote! { #impl_block });
612    }
613
614    // Generate event_types()
615    let event_type_exprs: Vec<_> = all_event_idents
616        .iter()
617        .map(|i| quote! { blvm_node::module::traits::EventType::#i })
618        .collect();
619
620    let event_types_fn: ImplItem = syn::parse2(quote! {
621        /// Event types to subscribe to (from #[on_event] handlers).
622        pub fn event_types() -> Vec<blvm_node::module::traits::EventType> {
623            vec![#(#event_type_exprs),*]
624        }
625    })
626    .expect("event_types fn should parse");
627
628    // Generate dispatch_event - match on event_type and call handler(s) with DI
629    let mut match_arms = Vec::new();
630    for (ev_key, method_infos) in &event_to_methods {
631        let ev_ident: syn::Ident = syn::parse_str(ev_key).unwrap();
632        let payload_fields = event_payload_map::payload_fields_for_event(ev_key);
633
634        let method_calls: Vec<proc_macro2::TokenStream> = method_infos
635            .iter()
636            .map(|(method_ident, params, event_types_for_method)| {
637                build_handler_call(
638                    method_ident,
639                    params,
640                    event_types_for_method,
641                    ev_key,
642                    &payload_fields,
643                )
644            })
645            .collect();
646
647        match_arms.push(quote! {
648            blvm_node::module::traits::EventType::#ev_ident => {
649                #(#method_calls)*
650            }
651        });
652    }
653    match_arms.push(quote! { _ => {} });
654
655    let dispatch_fn: ImplItem = syn::parse2(quote! {
656        /// Dispatch event to #[on_event] handlers.
657        pub async fn dispatch_event(
658            &self,
659            event: blvm_node::module::ipc::protocol::EventMessage,
660        ) -> Result<(), blvm_node::module::traits::ModuleError> {
661            use blvm_node::module::traits::EventType;
662            match event.event_type {
663                #(#match_arms),*
664            }
665            Ok(())
666        }
667    })
668    .expect("dispatch_event fn should parse");
669
670    impl_block.items.push(event_types_fn);
671    impl_block.items.push(dispatch_fn);
672
673    TokenStream::from(quote! { #impl_block })
674}
675
676fn parse_handler_params(method: &syn::ImplItemFn) -> Vec<(String, bool)> {
677    let mut out = Vec::new();
678    for arg in method.sig.inputs.iter().skip(1) {
679        if let syn::FnArg::Typed(pat_type) = arg {
680            let name = match &*pat_type.pat {
681                syn::Pat::Ident(pi) => pi.ident.to_string(),
682                _ => continue,
683            };
684            let is_event = matches!(
685                &*pat_type.ty,
686                syn::Type::Reference(tr) if matches!(&*tr.elem, syn::Type::Path(tp) if tp.path.is_ident("EventMessage"))
687            );
688            out.push((name, is_event));
689        }
690    }
691    out
692}
693
694fn build_handler_call(
695    method_ident: &syn::Ident,
696    params: &[(String, bool)],
697    event_types_for_method: &[String],
698    ev_key: &str,
699    payload_fields: &Option<Vec<(&'static str, bool)>>,
700) -> proc_macro2::TokenStream {
701    let use_di = event_types_for_method.len() == 1
702        && payload_fields.is_some()
703        && params.iter().all(|(name, is_event)| {
704            if *is_event {
705                true
706            } else {
707                payload_fields
708                    .as_ref()
709                    .unwrap()
710                    .iter()
711                    .any(|(f, _)| f == name)
712            }
713        });
714
715    if !use_di {
716        return quote! { self.#method_ident(&event).await?; };
717    }
718
719    let fields = payload_fields.as_ref().unwrap();
720    let field_idents: Vec<syn::Ident> = fields
721        .iter()
722        .map(|(f, _)| syn::Ident::new(f, proc_macro2::Span::call_site()))
723        .collect();
724    let ev_ident = syn::Ident::new(ev_key, proc_macro2::Span::call_site());
725
726    let call_args: Vec<proc_macro2::TokenStream> = params
727        .iter()
728        .map(|(name, is_event)| {
729            if *is_event {
730                quote! { &event }
731            } else {
732                let ident = syn::Ident::new(name, proc_macro2::Span::call_site());
733                let (_, is_copy) = fields.iter().find(|(f, _)| *f == name).unwrap();
734                if *is_copy {
735                    quote! { *#ident }
736                } else {
737                    quote! { #ident }
738                }
739            }
740        })
741        .collect();
742
743    quote! {
744        if let blvm_node::module::ipc::protocol::EventPayload::#ev_ident { #(#field_idents),* } = &event.payload {
745            self.#method_ident(#(#call_args),*).await?;
746        }
747    }
748}
749
750fn parse_on_event_args(attr: &syn::Attribute) -> Vec<syn::Ident> {
751    let parser = Punctuated::<syn::Ident, Comma>::parse_terminated;
752    attr.parse_args_with(parser)
753        .map(|p| p.into_iter().collect())
754        .unwrap_or_default()
755}
756
757/// Parameter attribute: `#[arg(long)]`, `#[arg(short = 'o')]`, `#[arg(default = "x")]`.
758/// Use on CLI handler parameters for named-arg parsing and type coercion.
759#[proc_macro_attribute]
760pub fn arg(_attr: TokenStream, item: TokenStream) -> TokenStream {
761    item
762}
763
764/// Field attribute: `#[config_env]` or `#[config_env("ENV_NAME")]`.
765/// Use with `#[module_config(name = "...")]` on the struct. Pass-through; the real logic
766/// is in `module_config` which reads this attribute to generate `apply_env_overrides()`.
767#[proc_macro_attribute]
768pub fn config_env(_attr: TokenStream, item: TokenStream) -> TokenStream {
769    item
770}
771
772/// Placeholder derive macro for future use.
773#[proc_macro_derive(ModuleCliSpec)]
774pub fn derive_module_cli_spec(input: TokenStream) -> TokenStream {
775    let _input = parse_macro_input!(input as DeriveInput);
776    quote! {}.into()
777}