1mod 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#[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#[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 pub const CLI_NAME: &str = #name_lit;
53 }
54 };
55
56 TokenStream::from(quote! {
57 #item_tokens
58 #impl_block
59 })
60}
61
62#[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 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
144pub(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
156fn 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
168pub(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
188fn 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#[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#[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#[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 TokenStream::from(quote! { #item })
297}
298
299fn 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 return Some(match &attr.meta {
307 Meta::Path(_) => None, 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
329fn 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#[proc_macro_attribute]
385pub fn config(attr: TokenStream, item: TokenStream) -> TokenStream {
386 module_config(attr, item)
387}
388
389fn 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 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 pub fn apply_env_overrides(&mut self) {}
440 }
441 } else {
442 quote! {
443 pub fn apply_env_overrides(&mut self) {
446 #(#apply_stmts)*
447 }
448 }
449 };
450
451 let load_block = quote! {
452 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 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#[proc_macro_attribute]
490pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
491 blvm_module(attr, item)
492}
493
494#[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#[proc_macro_attribute]
564pub fn on_event(_attr: TokenStream, item: TokenStream) -> TokenStream {
565 item
566}
567
568#[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 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 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 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 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 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#[proc_macro_attribute]
760pub fn arg(_attr: TokenStream, item: TokenStream) -> TokenStream {
761 item
762}
763
764#[proc_macro_attribute]
768pub fn config_env(_attr: TokenStream, item: TokenStream) -> TokenStream {
769 item
770}
771
772#[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}