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