Skip to main content

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}