anycms_event_derive/lib.rs
1//! Procedural macros for the anycms-event crate.
2//!
3//! Provides:
4//! - `#[derive(Event)]` — auto-implement the `Event` trait
5//! - `event_bus! { ... }` — define a typed event bus with compile-time guarantees
6//!
7//! # `#[derive(Event)]`
8//!
9//! Generates an `impl Event for YourStruct` block with `event_name()` and `topic()`.
10//!
11//! ## Attributes
12//!
13//! - `#[event(name = "user.created")]` — set the event name explicitly
14//! - `#[event(topic = "user")]` — set the topic explicitly
15//!
16//! If `name` is not specified, it is derived from the struct name using CamelCase convention:
17//! `UserCreated` -> `"user.created"`, `OrderPlaced` -> `"order.placed"`.
18//!
19//! If `topic` is not specified, it defaults to the first segment of the event name
20//! (e.g., `"user.created"` -> `"user"`).
21//!
22//! # `event_bus!`
23//!
24//! Define event structs, a topic enum, and a typed event bus in one declaration.
25//!
26//! ## Syntax
27//!
28//! ```ignore
29//! event_bus! {
30//! bus AppEventBus {
31//! event UserCreated { user_id: String, username: String }
32//! event UserDeleted { user_id: String, reason: String }
33//!
34//! // topic <method_name> => [EventType1, EventType2]
35//! topic user_events => [UserCreated, UserDeleted]
36//! }
37//! }
38//! ```
39//!
40//! This generates:
41//! - Struct definitions with `#[derive(Debug, Clone, Serialize, Deserialize)]`
42//! - `impl Event for ...` blocks
43//! - A topic enum `AppEventBusTopicEvent`
44//! - A typed `AppEventBus` newtype wrapping `anycms_event::EventBus`
45//! - A `subscribe_topic_user_events()` method for the topic group
46
47use proc_macro::TokenStream;
48use quote::quote;
49use syn::{
50 braced, parse, parse_macro_input, Data, DeriveInput, Expr, ExprLit, Ident, Lit,
51 Meta, MetaNameValue, Path, Token, Type,
52};
53
54// ---------------------------------------------------------------------------
55// CamelCase -> snake_case (dotted) conversion
56// ---------------------------------------------------------------------------
57
58/// Convert an UpperCamelCase identifier into a dotted lowercase string.
59///
60/// Rules:
61/// - Each uppercase letter starts a new segment (unless it's part of a run
62/// like `HTTPServer` -> `http_server` -> `http.server`).
63/// - Segments are joined with `"."`.
64/// - The entire result is lowercase.
65///
66/// Examples:
67/// `UserCreated` -> `"user.created"`
68/// `OrderPlaced` -> `"order.placed"`
69/// `HTTPServer` -> `"http.server"`
70fn camel_to_dotted(name: &str) -> String {
71 let mut result = String::with_capacity(name.len() + 8);
72 let mut chars = name.chars().peekable();
73
74 // Track whether the previous char was lowercase (or digit).
75 let mut prev_lower = false;
76
77 while let Some(ch) = chars.next() {
78 if ch.is_uppercase() {
79 // Insert a segment separator if:
80 // - we are not at the start, AND
81 // - the previous char was lowercase OR the next char is lowercase
82 // (handles "HTTPServer" -> "H-T-T-P-Server" with proper splits)
83 let next_lower = chars.peek().is_some_and(|c| c.is_lowercase());
84 if prev_lower || next_lower {
85 if !result.is_empty() {
86 result.push('.');
87 }
88 } else if !result.is_empty() {
89 // We're in an all-uppercase run (e.g., "HTTP").
90 // Don't split — just append the lowercase char.
91 } else {
92 // first char, no separator
93 }
94 result.push(ch.to_ascii_lowercase());
95 prev_lower = false;
96 } else {
97 // Lowercase or digit.
98 if result.is_empty() {
99 // first char, just push
100 }
101 result.push(ch);
102 prev_lower = ch.is_lowercase() || ch.is_ascii_digit();
103 }
104 }
105
106 result
107}
108
109// ---------------------------------------------------------------------------
110// Attribute parsing helpers
111// ---------------------------------------------------------------------------
112
113/// Parsed `#[event(..)]` attributes.
114struct EventAttrs {
115 /// Explicit event name, e.g. `#[event(name = "user.created")]`.
116 name: Option<String>,
117 /// Explicit topic, e.g. `#[event(topic = "user")]`.
118 topic: Option<String>,
119}
120
121/// Parse all `#[event(...)]` attributes on the struct.
122fn parse_event_attrs(attrs: &[syn::Attribute]) -> EventAttrs {
123 let mut name = None;
124 let mut topic = None;
125
126 for attr in attrs {
127 if !attr.path().is_ident("event") {
128 continue;
129 }
130
131 // Parse the contents as a comma-separated list of `key = "value"`.
132 let nested = attr.parse_args_with(
133 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
134 );
135
136 if let Ok(metas) = nested {
137 for meta in metas {
138 if let Meta::NameValue(MetaNameValue {
139 path,
140 value:
141 Expr::Lit(ExprLit {
142 lit: Lit::Str(lit_str),
143 ..
144 }),
145 ..
146 }) = meta
147 {
148 if path.is_ident("name") {
149 name = Some(lit_str.value());
150 } else if path.is_ident("topic") {
151 topic = Some(lit_str.value());
152 }
153 }
154 }
155 }
156 }
157
158 EventAttrs { name, topic }
159}
160
161/// Extract the first dotted segment as the default topic.
162///
163/// `"user.created"` -> `"user"`
164/// `"order.placed"` -> `"order"`
165/// `"order"` -> `"order"`
166fn first_segment(name: &str) -> &str {
167 name.split('.').next().unwrap_or(name)
168}
169
170// ---------------------------------------------------------------------------
171// The derive macro
172// ---------------------------------------------------------------------------
173
174/// Derive macro for the `Event` trait.
175///
176/// See the crate-level documentation for usage.
177#[proc_macro_derive(Event, attributes(event))]
178pub fn derive_event(input: TokenStream) -> TokenStream {
179 let input = parse_macro_input!(input as DeriveInput);
180
181 // Only structs are supported.
182 match &input.data {
183 Data::Struct(_) => {}
184 Data::Enum(_) => {
185 return syn::Error::new_spanned(
186 &input.ident,
187 "Event can only be derived for structs, not enums",
188 )
189 .to_compile_error()
190 .into();
191 }
192 Data::Union(_) => {
193 return syn::Error::new_spanned(
194 &input.ident,
195 "Event can only be derived for structs, not unions",
196 )
197 .to_compile_error()
198 .into();
199 }
200 }
201
202 let ident = &input.ident;
203 let attrs = parse_event_attrs(&input.attrs);
204
205 // Determine event_name.
206 let event_name = attrs.name.unwrap_or_else(|| camel_to_dotted(&ident.to_string()));
207
208 // Determine topic.
209 let topic = attrs
210 .topic
211 .unwrap_or_else(|| first_segment(&event_name).to_owned());
212
213 let expanded = quote! {
214 impl ::anycms_event::Event for #ident {
215 fn event_name() -> &'static str {
216 #event_name
217 }
218
219 fn topic() -> &'static str {
220 #topic
221 }
222
223 fn to_json(&self) -> Option<::serde_json::Value> {
224 ::serde_json::to_value(self).ok()
225 }
226
227 fn from_json(json: &str) -> Option<Self>
228 where
229 Self: Sized,
230 {
231 ::serde_json::from_str(json).ok()
232 }
233 }
234 };
235
236 expanded.into()
237}
238
239// ---------------------------------------------------------------------------
240// event_bus! macro — parser
241// ---------------------------------------------------------------------------
242
243/// A single `event Name { fields }` declaration inside the macro.
244struct EventDecl {
245 name: Ident,
246 fields: Vec<(Ident, Type)>,
247}
248
249/// A single `topic name => [EventType1, EventType2]` declaration.
250struct TopicDecl {
251 /// User-specified method name suffix (the identifier after `topic`).
252 method_name: Ident,
253 event_types: Vec<Ident>,
254}
255
256/// The full parsed `event_bus!` input.
257struct EventBusDef {
258 bus_name: Ident,
259 events: Vec<EventDecl>,
260 topics: Vec<TopicDecl>,
261 /// Whether the `redis` attribute was specified: `bus MyBus(redis) { ... }`
262 enable_redis: bool,
263}
264
265/// Helper: peek if the next token is a specific identifier (e.g. "bus", "event", "topic").
266fn peek_keyword(input: &parse::ParseBuffer, keyword: &str) -> bool {
267 input.peek(Ident)
268 && input
269 .cursor()
270 .ident()
271 .is_some_and(|(ident, _)| ident == keyword)
272}
273
274/// Helper: parse a specific identifier keyword or error.
275fn parse_keyword(input: parse::ParseStream, keyword: &str) -> syn::Result<()> {
276 let ident: Ident = input.parse()?;
277 if ident != keyword {
278 return Err(syn::Error::new(ident.span(), format!("expected `{}`", keyword)));
279 }
280 Ok(())
281}
282
283impl parse::Parse for EventBusDef {
284 fn parse(input: parse::ParseStream) -> syn::Result<Self> {
285 // Expect `bus Ident` or `bus Ident(redis) { ... }`
286 parse_keyword(input, "bus")?;
287 let bus_name: Ident = input.parse()?;
288
289 // Optional `(redis)` attribute
290 let mut enable_redis = false;
291 if input.peek(syn::token::Paren) {
292 let paren_content;
293 syn::parenthesized!(paren_content in input);
294 let attr: Ident = paren_content.parse()?;
295 if attr != "redis" {
296 return Err(syn::Error::new(attr.span(), "expected `redis`"));
297 }
298 enable_redis = true;
299 }
300
301 let content;
302 braced!(content in input);
303
304 let mut events = Vec::new();
305 let mut topics = Vec::new();
306
307 while !content.is_empty() {
308 if peek_keyword(&content, "event") {
309 // Parse `event Ident { field: Type, ... }`
310 parse_keyword(&content, "event")?;
311 let name: Ident = content.parse()?;
312
313 let fields_content;
314 braced!(fields_content in content);
315
316 let mut fields = Vec::new();
317 while !fields_content.is_empty() {
318 let field_name: Ident = fields_content.parse()?;
319 fields_content.parse::<Token![:]>()?;
320 let field_type: Type = fields_content.parse()?;
321
322 fields.push((field_name, field_type));
323
324 // Optional trailing comma
325 if fields_content.peek(Token![,]) {
326 fields_content.parse::<Token![,]>()?;
327 }
328 }
329
330 events.push(EventDecl { name, fields });
331 } else if peek_keyword(&content, "topic") {
332 // Parse `topic name => [EventType1, EventType2]`
333 parse_keyword(&content, "topic")?;
334 let method_name: Ident = content.parse()?;
335 content.parse::<Token![=>]>()?;
336
337 let types_content;
338 let _bracket = syn::bracketed!(types_content in content);
339
340 let mut event_types = Vec::new();
341 while !types_content.is_empty() {
342 let event_type: Path = types_content.parse()?;
343 // Extract the final ident from the path
344 if let Some(segment) = event_type.segments.last() {
345 event_types.push(segment.ident.clone());
346 }
347 // Optional trailing comma
348 if types_content.peek(Token![,]) {
349 types_content.parse::<Token![,]>()?;
350 }
351 }
352
353 topics.push(TopicDecl {
354 method_name,
355 event_types,
356 });
357 } else {
358 return Err(content.error("expected `event` or `topic`"));
359 }
360 }
361
362 Ok(EventBusDef {
363 bus_name,
364 events,
365 topics,
366 enable_redis,
367 })
368 }
369}
370
371// ---------------------------------------------------------------------------
372// event_bus! macro — code generation
373// ---------------------------------------------------------------------------
374
375/// Define a typed event bus with events and topic groupings.
376#[proc_macro]
377pub fn event_bus(input: TokenStream) -> TokenStream {
378 let def = match syn::parse::<EventBusDef>(input) {
379 Ok(d) => d,
380 Err(e) => return e.to_compile_error().into(),
381 };
382
383 let bus_name = &def.bus_name;
384 let enum_name = quote::format_ident!("{}TopicEvent", def.bus_name);
385
386 // ------------------------------------------------------------------
387 // 1. Generate event structs + Event impls
388 // ------------------------------------------------------------------
389 let event_structs: Vec<proc_macro2::TokenStream> = def
390 .events
391 .iter()
392 .map(|event| {
393 let name = &event.name;
394 let event_name_str = camel_to_dotted(&name.to_string());
395 let topic_str = first_segment(&event_name_str).to_owned();
396
397 let fields: Vec<proc_macro2::TokenStream> = event
398 .fields
399 .iter()
400 .map(|(fname, ftype)| {
401 quote! { pub #fname: #ftype }
402 })
403 .collect();
404
405 quote! {
406 #[derive(::std::fmt::Debug, ::std::clone::Clone, ::serde::Serialize, ::serde::Deserialize)]
407 pub struct #name {
408 #(#fields),*
409 }
410
411 impl ::anycms_event::Event for #name {
412 fn event_name() -> &'static str {
413 #event_name_str
414 }
415
416 fn topic() -> &'static str {
417 #topic_str
418 }
419
420 fn to_json(&self) -> Option<::serde_json::Value> {
421 ::serde_json::to_value(self).ok()
422 }
423
424 fn from_json(json: &str) -> Option<Self>
425 where
426 Self: Sized,
427 {
428 ::serde_json::from_str(json).ok()
429 }
430 }
431 }
432 })
433 .collect();
434
435 // ------------------------------------------------------------------
436 // 2. Generate the topic enum (only if there are topics)
437 // ------------------------------------------------------------------
438 let topic_enum = if def.topics.is_empty() {
439 quote! {}
440 } else {
441 let variants: Vec<proc_macro2::TokenStream> = def
442 .topics
443 .iter()
444 .flat_map(|topic| &topic.event_types)
445 .map(|event_type| {
446 quote! { #event_type(#event_type) }
447 })
448 .collect();
449
450 // Only generate the enum if we have variants
451 if variants.is_empty() {
452 quote! {}
453 } else {
454 quote! {
455 #[derive(::std::fmt::Debug, ::std::clone::Clone, ::serde::Serialize, ::serde::Deserialize)]
456 #[serde(tag = "event_type")]
457 pub enum #enum_name {
458 #(#variants),*
459 }
460 }
461 }
462 };
463
464 // ------------------------------------------------------------------
465 // 3. Generate per-topic subscribe methods
466 // ------------------------------------------------------------------
467 let topic_subscribe_methods: Vec<proc_macro2::TokenStream> = if def.topics.is_empty() || def.topics.iter().all(|t| t.event_types.is_empty()) {
468 Vec::new()
469 } else {
470 def.topics
471 .iter()
472 .map(|topic| {
473 let method_name = quote::format_ident!("subscribe_topic_{}", topic.method_name);
474
475 let subscribe_arms: Vec<proc_macro2::TokenStream> = topic
476 .event_types
477 .iter()
478 .map(|event_type| {
479 let variant = event_type;
480 quote! {
481 {
482 let h = handler.clone();
483 self.inner.subscribe::<#variant, _, _>(move |e| {
484 let h = h.clone();
485 async move { h(#enum_name::#variant(e)).await }
486 }).await
487 }
488 }
489 })
490 .collect();
491
492 quote! {
493 pub async fn #method_name<F, Fut>(&self, handler: F) -> ::std::vec::Vec<::anycms_event::Result<::anycms_event::bus::Subscription>>
494 where
495 F: Fn(#enum_name) -> Fut + ::std::clone::Clone + ::std::marker::Send + ::std::marker::Sync + 'static,
496 Fut: ::std::future::Future<Output = ::anycms_event::Result<()>> + ::std::marker::Send + 'static,
497 {
498 let mut subs = ::std::vec::Vec::new();
499 #(
500 subs.push(#subscribe_arms);
501 )*
502 subs
503 }
504 }
505 })
506 .collect()
507 };
508
509 // ------------------------------------------------------------------
510 // 4. Generate redis support (only when `redis` attr is set)
511 // ------------------------------------------------------------------
512 let forward_calls: Vec<proc_macro2::TokenStream> = def
513 .events
514 .iter()
515 .map(|event| {
516 let name = &event.name;
517 quote! {
518 handles.push(bridged.forward_from_redis::<#name>().await?);
519 }
520 })
521 .collect();
522
523 let redis_impl = if def.enable_redis && !def.events.is_empty() {
524
525 // Generate: bridge() method on the typed bus + Bridged wrapper type
526 let bridged_name = quote::format_ident!("Bridged{}", def.bus_name);
527
528 quote! {
529 /// A [`::anycms_event_redis::BridgedEventBus`] with all event types
530 /// automatically forwarded from Redis to the local bus.
531 ///
532 /// Created via [`#bus_name::bridge`]. Supports the same `publish` and
533 /// `subscribe` operations as a plain bus, with events also sent to/received
534 /// from Redis.
535 pub struct #bridged_name {
536 inner: ::anycms_event_redis::BridgedEventBus,
537 _forwarder_handles: ::std::vec::Vec<::anycms_event_redis::ForwarderHandle>,
538 }
539
540 impl #bridged_name {
541 /// Publish an event to both local subscribers and Redis.
542 pub async fn publish<E>(&self, event: E) -> ::anycms_event::Result<()>
543 where
544 E: ::anycms_event::Event + ::std::clone::Clone
545 + ::serde::Serialize + ::serde::de::DeserializeOwned,
546 {
547 self.inner.publish(event).await
548 }
549
550 /// Subscribe to a specific event type on the local bus.
551 pub async fn subscribe<E, F, Fut>(&self, handler: F) -> ::anycms_event::Result<::anycms_event::bus::Subscription>
552 where
553 E: ::anycms_event::Event,
554 F: Fn(E) -> Fut + ::std::marker::Send + ::std::marker::Sync + 'static,
555 Fut: ::std::future::Future<Output = ::anycms_event::Result<()>> + ::std::marker::Send + 'static,
556 {
557 self.inner.subscribe::<E, F, Fut>(handler).await
558 }
559
560 /// Access the underlying [`::anycms_event_redis::BridgedEventBus`].
561 pub fn inner(&self) -> &::anycms_event_redis::BridgedEventBus {
562 &self.inner
563 }
564 }
565
566 impl ::std::clone::Clone for #bridged_name {
567 fn clone(&self) -> Self {
568 Self {
569 inner: self.inner.clone(),
570 // Forwarder handles are not clonable — the clone shares the
571 // same forwarders from the original instance.
572 _forwarder_handles: ::std::vec::Vec::new(),
573 }
574 }
575 }
576 }
577 } else {
578 quote! {}
579 };
580
581 let redis_bridge_method = if def.enable_redis && !def.events.is_empty() {
582 let bridged_name = quote::format_ident!("Bridged{}", def.bus_name);
583
584 quote! {
585 /// Bridge this bus with a Redis transport, automatically forwarding all
586 /// event types from Redis to the local bus.
587 ///
588 /// Returns a [`#bridged_name`] that supports `publish` and `subscribe`
589 /// with Redis integration. No need to call `forward_from_redis` manually.
590 ///
591 /// # Example
592 ///
593 /// ```ignore
594 /// let transport = RedisTransport::new("redis://127.0.0.1:6379").await?;
595 /// let bus = AppEventBus::new();
596 /// let bridged = bus.bridge(&transport).await?;
597 ///
598 /// bridged.subscribe(|e: UserCreated| async move { Ok(()) }).await?;
599 /// bridged.publish(UserCreated { ... }).await?; // local + Redis
600 /// ```
601 pub async fn bridge(
602 &self,
603 transport: &::anycms_event_redis::RedisTransport,
604 ) -> ::std::result::Result<#bridged_name, ::anycms_event_redis::RedisTransportError> {
605 let bridged = transport.bridge(self.inner.clone()).await?;
606 let mut handles = ::std::vec::Vec::new();
607 #(#forward_calls)*
608 Ok(#bridged_name {
609 inner: bridged,
610 _forwarder_handles: handles,
611 })
612 }
613 }
614 } else {
615 quote! {}
616 };
617
618 // ------------------------------------------------------------------
619 // 5. Generate the typed EventBus newtype
620 // ------------------------------------------------------------------
621 let bus_impl = quote! {
622 pub struct #bus_name {
623 inner: ::anycms_event::EventBus,
624 }
625
626 impl #bus_name {
627 pub fn new() -> Self {
628 Self {
629 inner: ::anycms_event::EventBus::new(),
630 }
631 }
632
633 /// Get a reference to the underlying [`::anycms_event::EventBus`].
634 pub fn inner(&self) -> &::anycms_event::EventBus {
635 &self.inner
636 }
637
638 /// Consume this typed bus and return the underlying [`::anycms_event::EventBus`].
639 pub fn into_inner(self) -> ::anycms_event::EventBus {
640 self.inner
641 }
642
643 pub async fn publish<E: ::anycms_event::Event>(&self, event: E) -> ::anycms_event::Result<()> {
644 self.inner.publish(event).await
645 }
646
647 pub async fn subscribe<E, F, Fut>(&self, handler: F) -> ::anycms_event::Result<::anycms_event::bus::Subscription>
648 where
649 E: ::anycms_event::Event,
650 F: Fn(E) -> Fut + ::std::marker::Send + ::std::marker::Sync + 'static,
651 Fut: ::std::future::Future<Output = ::anycms_event::Result<()>> + ::std::marker::Send + 'static,
652 {
653 self.inner.subscribe::<E, F, Fut>(handler).await
654 }
655
656 #(#topic_subscribe_methods)*
657
658 #redis_bridge_method
659 }
660
661 impl ::std::clone::Clone for #bus_name {
662 fn clone(&self) -> Self {
663 Self {
664 inner: self.inner.clone(),
665 }
666 }
667 }
668
669 impl ::std::default::Default for #bus_name {
670 fn default() -> Self {
671 Self::new()
672 }
673 }
674 };
675
676 // ------------------------------------------------------------------
677 // Assemble everything
678 // ------------------------------------------------------------------
679 let expanded = quote! {
680 #(#event_structs)*
681 #topic_enum
682 #bus_impl
683 #redis_impl
684 };
685
686 expanded.into()
687}
688
689// ---------------------------------------------------------------------------
690// Tests (run with `cargo test -p anycms-event-derive`)
691// ---------------------------------------------------------------------------
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_camel_to_dotted_simple() {
699 assert_eq!(camel_to_dotted("UserCreated"), "user.created");
700 }
701
702 #[test]
703 fn test_camel_to_dotted_three_words() {
704 assert_eq!(camel_to_dotted("UserProfileUpdated"), "user.profile.updated");
705 }
706
707 #[test]
708 fn test_camel_to_dotted_single_word() {
709 assert_eq!(camel_to_dotted("Order"), "order");
710 }
711
712 #[test]
713 fn test_camel_to_dotted_acronym() {
714 assert_eq!(camel_to_dotted("HTTPServer"), "http.server");
715 }
716
717 #[test]
718 fn test_camel_to_dotted_order_placed() {
719 assert_eq!(camel_to_dotted("OrderPlaced"), "order.placed");
720 }
721
722 #[test]
723 fn test_first_segment_simple() {
724 assert_eq!(first_segment("user.created"), "user");
725 }
726
727 #[test]
728 fn test_first_segment_single() {
729 assert_eq!(first_segment("order"), "order");
730 }
731}