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
16#[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#[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 pub const CLI_NAME: &str = #name_lit;
51 }
52 };
53
54 TokenStream::from(quote! {
55 #item_tokens
56 #impl_block
57 })
58}
59
60#[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 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
142pub(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
154fn 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
166pub(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
186fn 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#[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#[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#[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 TokenStream::from(quote! { #item })
295}
296
297fn 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 return Some(match &attr.meta {
305 Meta::Path(_) => None, 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
327fn 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#[proc_macro_attribute]
381pub fn config(attr: TokenStream, item: TokenStream) -> TokenStream {
382 module_config(attr, item)
383}
384
385fn 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 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 pub fn apply_env_overrides(&mut self) {}
436 }
437 } else {
438 quote! {
439 pub fn apply_env_overrides(&mut self) {
442 #(#apply_stmts)*
443 }
444 }
445 };
446
447 let load_block = quote! {
448 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 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#[proc_macro_attribute]
486pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
487 blvm_module(attr, item)
488}
489
490#[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#[proc_macro_attribute]
560pub fn on_event(_attr: TokenStream, item: TokenStream) -> TokenStream {
561 item
562}
563
564#[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 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 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 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 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 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#[proc_macro_attribute]
757pub fn arg(_attr: TokenStream, item: TokenStream) -> TokenStream {
758 item
759}
760
761#[proc_macro_attribute]
765pub fn config_env(_attr: TokenStream, item: TokenStream) -> TokenStream {
766 item
767}
768
769#[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}