evento_macro/
lib.rs

1//! Procedural macros for the Evento event sourcing framework.
2//!
3//! This crate provides macros that eliminate boilerplate when building event-sourced
4//! applications with Evento. It generates trait implementations, handler structs,
5//! and serialization code automatically.
6//!
7//! # Macros
8//!
9//! | Macro | Type | Purpose |
10//! |-------|------|---------|
11//! | [`aggregator`] | Attribute | Transform enum into event structs with trait impls |
12//! | [`handler`] | Attribute | Create event handler from async function |
13//! | [`snapshot`] | Attribute | Implement snapshot restoration for projections |
14//! | [`debug_handler`] | Attribute | Like `handler` but outputs generated code for debugging |
15//! | [`debug_snapshot`] | Attribute | Like `snapshot` but outputs generated code for debugging |
16//!
17//! # Usage
18//!
19//! This crate is typically used through the main `evento` crate with the `macro` feature
20//! enabled (on by default):
21//!
22//! ```toml
23//! [dependencies]
24//! evento = "1.8"
25//! ```
26//!
27//! # Examples
28//!
29//! ## Defining Events with `#[evento::aggregator]`
30//!
31//! Transform an enum into individual event structs:
32//!
33//! ```rust,ignore
34//! #[evento::aggregator]
35//! pub enum BankAccount {
36//!     /// Event raised when a new bank account is opened
37//!     AccountOpened {
38//!         owner_id: String,
39//!         owner_name: String,
40//!         initial_balance: i64,
41//!     },
42//!
43//!     MoneyDeposited {
44//!         amount: i64,
45//!         transaction_id: String,
46//!     },
47//!
48//!     MoneyWithdrawn {
49//!         amount: i64,
50//!         transaction_id: String,
51//!     },
52//! }
53//! ```
54//!
55//! This generates:
56//! - `AccountOpened`, `MoneyDeposited`, `MoneyWithdrawn` structs
57//! - `Aggregator` and `Event` trait implementations for each
58//! - Automatic derives: `Debug`, `Clone`, `PartialEq`, `Default`, and rkyv serialization
59//!
60//! ## Creating Handlers with `#[evento::handler]`
61//!
62//! ```rust,ignore
63//! #[evento::handler]
64//! async fn handle_money_deposited<E: Executor>(
65//!     event: Event<MoneyDeposited>,
66//!     action: Action<'_, AccountBalanceView, E>,
67//! ) -> anyhow::Result<()> {
68//!     match action {
69//!         Action::Apply(row) => {
70//!             row.balance += event.data.amount;
71//!         }
72//!         Action::Handle(_context) => {}
73//!     };
74//!     Ok(())
75//! }
76//!
77//! // Use in a projection
78//! let projection = Projection::new("account-balance")
79//!     .handler(handle_money_deposited());
80//! ```
81//!
82//! ## Snapshot Restoration with `#[evento::snapshot]`
83//!
84//! ```rust,ignore
85//! #[evento::snapshot]
86//! async fn restore(
87//!     context: &evento::context::RwContext,
88//!     id: String,
89//! ) -> anyhow::Result<Option<LoadResult<AccountBalanceView>>> {
90//!     // Load snapshot from database or return None
91//!     Ok(None)
92//! }
93//! ```
94//!
95//! # Requirements
96//!
97//! When using these macros, your types must meet certain requirements:
98//!
99//! - **Events** (from `#[aggregator]`): Automatically derive required traits
100//! - **Projections**: Must implement `Default`, `Send`, `Sync`, `Clone`
101//! - **Handler functions**: Must be `async` and return `anyhow::Result<()>`
102//!
103//! # Serialization
104//!
105//! Events are serialized using [rkyv](https://rkyv.org/) for zero-copy deserialization.
106//! The `#[aggregator]` macro automatically adds the required rkyv derives.
107
108use convert_case::{Case, Casing};
109use proc_macro::TokenStream;
110use quote::{format_ident, quote};
111use syn::{
112    parse_macro_input, punctuated::Punctuated, Error, Fields, FnArg, GenericArgument, ItemEnum,
113    ItemFn, Meta, PatType, PathArguments, ReturnType, Token, Type, TypePath,
114};
115
116/// Transforms an enum into individual event structs with trait implementations.
117///
118/// This macro takes an enum where each variant represents an event type and generates:
119/// - Individual public structs for each variant
120/// - `Aggregator` trait implementation (provides `aggregator_type()`)
121/// - `Event` trait implementation (provides `event_name()`)
122/// - Automatic derives: `Debug`, `Clone`, `PartialEq`, `Default`, and rkyv serialization
123///
124/// # Aggregator Type Format
125///
126/// The aggregator type is formatted as `"{package_name}/{enum_name}"`, e.g., `"bank/BankAccount"`.
127///
128/// # Example
129///
130/// ```rust,ignore
131/// #[evento::aggregator]
132/// pub enum BankAccount {
133///     /// Event raised when account is opened
134///     AccountOpened {
135///         owner_id: String,
136///         owner_name: String,
137///         initial_balance: i64,
138///     },
139///
140///     MoneyDeposited {
141///         amount: i64,
142///         transaction_id: String,
143///     },
144/// }
145///
146/// // Generated structs can be used directly:
147/// let event = AccountOpened {
148///     owner_id: "user123".into(),
149///     owner_name: "John".into(),
150///     initial_balance: 1000,
151/// };
152/// ```
153///
154/// # Additional Derives
155///
156/// Pass additional derives as arguments:
157///
158/// ```rust,ignore
159/// #[evento::aggregator(serde::Serialize, serde::Deserialize)]
160/// pub enum MyEvents {
161///     // variants...
162/// }
163/// ```
164///
165/// # Variant Types
166///
167/// Supports all enum variant types:
168/// - Named fields: `Variant { field: Type }`
169/// - Tuple fields: `Variant(Type1, Type2)`
170/// - Unit variants: `Variant`
171#[proc_macro_attribute]
172pub fn aggregator(attr: TokenStream, item: TokenStream) -> TokenStream {
173    let input = parse_macro_input!(item as ItemEnum);
174
175    let enum_name = &input.ident;
176    let enum_name_str = enum_name.to_string();
177    let vis = &input.vis;
178
179    let user_derives = if attr.is_empty() {
180        vec![]
181    } else {
182        let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
183        let parsed = syn::parse::Parser::parse(parser, attr).unwrap_or_default();
184        parsed.into_iter().collect::<Vec<_>>()
185    };
186
187    // Generate a struct for each variant
188    let structs = input.variants.iter().map(|variant| {
189        let variant_name = &variant.ident;
190        let variant_name_str = variant_name.to_string();
191        let attrs = &variant.attrs; // preserves doc comments
192
193        // Mandatory + user derives
194        let derives = if user_derives.is_empty() {
195            quote! { #[derive(Debug, Clone, PartialEq, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] }
196        } else {
197            quote! { #[derive(Debug, Clone, PartialEq, Default, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, #(#user_derives),*)] }
198        };
199
200        let impl_event = quote! {
201            impl evento::projection::Aggregator for #variant_name {
202                fn aggregator_type() -> &'static str {
203                    static NAME: std::sync::LazyLock<String> = std::sync::LazyLock::new(||{
204                        format!("{}/{}", env!("CARGO_PKG_NAME"), #enum_name_str)
205                    });
206
207                    &NAME
208                }
209            }
210
211            impl evento::projection::Event for #variant_name {
212                fn event_name() -> &'static str {
213                    #variant_name_str
214                }
215            }
216        };
217
218        match &variant.fields {
219            Fields::Named(fields) => {
220                let fields = fields.named.iter().map(|f| {
221                    let field_name = &f.ident;
222                    let field_ty = &f.ty;
223                    let field_attrs = &f.attrs;
224                    quote! {
225                        #(#field_attrs)*
226                        pub #field_name: #field_ty
227                    }
228                });
229
230                quote! {
231                    #(#attrs)*
232                    #derives
233                    #vis struct #variant_name {
234                        #(#fields),*
235                    }
236                    #impl_event
237                }
238            }
239            Fields::Unnamed(fields) => {
240                let fields = fields.unnamed.iter().map(|f| {
241                    let field_ty = &f.ty;
242                    quote! { pub #field_ty }
243                });
244
245                quote! {
246                    #(#attrs)*
247                    #derives
248                    #vis struct #variant_name(#(#fields),*);
249                    #impl_event
250                }
251            }
252            Fields::Unit => {
253                quote! {
254                    #(#attrs)*
255                    #derives
256                    #vis struct #variant_name;
257                    #impl_event
258                }
259            }
260        }
261    });
262
263    // Optionally keep the original enum too
264    quote! {
265        #(#structs)*
266
267        #[derive(Default)]
268        #vis struct #enum_name;
269
270        impl evento::projection::Aggregator for #enum_name {
271            fn aggregator_type() -> &'static str {
272                static NAME: std::sync::LazyLock<String> = std::sync::LazyLock::new(||{
273                    format!("{}/{}", env!("CARGO_PKG_NAME"), #enum_name_str)
274                });
275
276                &NAME
277            }
278        }
279    }
280    .into()
281}
282
283/// Creates an event handler from an async function.
284///
285/// This macro transforms an async function into a handler struct that implements
286/// the `Handler<P, E>` trait for use with projections.
287///
288/// # Function Signature
289///
290/// The function must have this signature:
291///
292/// ```rust,ignore
293/// async fn handler_name<E: Executor>(
294///     event: Event<EventType>,
295///     action: Action<'_, ProjectionType, E>,
296/// ) -> anyhow::Result<()>
297/// ```
298///
299/// # Generated Code
300///
301/// For a function `handle_money_deposited`, the macro generates:
302/// - `HandleMoneyDepositedHandler` struct
303/// - `handle_money_deposited()` constructor function
304/// - `Handler<ProjectionType, E>` trait implementation
305///
306/// # Example
307///
308/// ```rust,ignore
309/// #[evento::handler]
310/// async fn handle_money_deposited<E: Executor>(
311///     event: Event<MoneyDeposited>,
312///     action: Action<'_, AccountBalanceView, E>,
313/// ) -> anyhow::Result<()> {
314///     match action {
315///         Action::Apply(row) => {
316///             row.balance += event.data.amount;
317///         }
318///         Action::Handle(_context) => {
319///             // Side effects, notifications, etc.
320///         }
321///     };
322///     Ok(())
323/// }
324///
325/// // Register with projection
326/// let projection = Projection::new("account-balance")
327///     .handler(handle_money_deposited());
328/// ```
329///
330/// # Action Variants
331///
332/// - `Action::Apply(row)` - Mutate projection state (for rebuilding from events)
333/// - `Action::Handle(context)` - Handle side effects during live processing
334#[proc_macro_attribute]
335pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
336    let input = parse_macro_input!(item as ItemFn);
337
338    match handler_next_impl(&input, false) {
339        Ok(tokens) => tokens,
340        Err(e) => e.to_compile_error().into(),
341    }
342}
343
344/// Debug variant of [`handler`] that writes generated code to a file.
345///
346/// The generated code is written to `target/evento_debug_handler_macro.rs`
347/// for inspection. Useful for understanding what the macro produces.
348///
349/// # Example
350///
351/// ```rust,ignore
352/// #[evento::debug_handler]
353/// async fn handle_event<E: Executor>(
354///     event: Event<MyEvent>,
355///     action: Action<'_, MyView, E>,
356/// ) -> anyhow::Result<()> {
357///     // ...
358/// }
359/// ```
360#[proc_macro_attribute]
361pub fn debug_handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
362    let input = parse_macro_input!(item as ItemFn);
363
364    match handler_next_impl(&input, true) {
365        Ok(tokens) => tokens,
366        Err(e) => e.to_compile_error().into(),
367    }
368}
369
370fn handler_next_impl(input: &ItemFn, debug: bool) -> syn::Result<TokenStream> {
371    let fn_name = &input.sig.ident;
372
373    // Extract parameters
374    let mut params = input.sig.inputs.iter();
375
376    // First param: Event<AccountOpened>
377    let event_arg = params.next().ok_or_else(|| {
378        Error::new_spanned(&input.sig, "expected first parameter: event: Event<T>")
379    })?;
380    let (event_full_type, event_inner_type) = extract_type_with_first_generic(event_arg)?;
381
382    // Second param: Action<'_, AccountBalanceView, E>
383    let action_arg = params.next().ok_or_else(|| {
384        Error::new_spanned(
385            &input.sig,
386            "expected second parameter: action: Action<'_, P, E>",
387        )
388    })?;
389    let projection_type = extract_projection_type(action_arg)?;
390
391    // Generate struct name: AccountOpened -> AccountOpenedHandler
392    let handler_struct = format_ident!("{}Handler", fn_name.to_string().to_case(Case::UpperCamel));
393
394    let output = quote! {
395        pub struct #handler_struct;
396
397        fn #fn_name() -> #handler_struct { #handler_struct }
398
399        impl #handler_struct {
400            #input
401        }
402
403        impl<E: ::evento::Executor> ::evento::projection::Handler<#projection_type, E> for #handler_struct {
404            fn apply<'a>(
405                &'a self,
406                projection: &'a mut #projection_type,
407                event: &'a ::evento::Event,
408            ) -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ::anyhow::Result<()>> + Send + 'a>> {
409                Box::pin(async move {
410                    let event: #event_full_type = match event.try_into() {
411                        Ok(data) => data,
412                        Err(e) => return Err(e.into()),
413                    };
414                    Self::#fn_name(
415                        event,
416                        ::evento::projection::Action::<'_, _, E>::Apply(projection),
417                    )
418                    .await
419                })
420            }
421
422            fn handle<'a>(
423                &'a self,
424                context: &'a ::evento::projection::Context<'a, E>,
425                event: &'a ::evento::Event,
426            ) -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ::anyhow::Result<()>> + Send + 'a>> {
427                Box::pin(async move {
428                    let event: #event_full_type = match event.try_into() {
429                        Ok(data) => data,
430                        Err(e) => return Err(e.into()),
431                    };
432                    Self::#fn_name(event, ::evento::projection::Action::Handle(context)).await
433                })
434            }
435
436            fn event_name(&self) -> &'static str {
437                use ::evento::projection::Event as _;
438                #event_inner_type::event_name()
439            }
440
441            fn aggregator_type(&self) -> &'static str {
442                use ::evento::projection::Aggregator as _;
443                #event_inner_type::aggregator_type()
444            }
445        }
446    };
447
448    if !debug {
449        return Ok(output.into());
450    }
451
452    let manifest_dir = env!("CARGO_MANIFEST_DIR");
453    let debug_path =
454        std::path::PathBuf::from(&manifest_dir).join("../target/evento_debug_handler_macro.rs"); // adjust ../ as needed
455
456    std::fs::write(&debug_path, output.to_string()).ok();
457
458    let debug_path_str = debug_path
459        .canonicalize()
460        .unwrap()
461        .to_string_lossy()
462        .to_string();
463
464    Ok(quote! {
465        include!(#debug_path_str);
466    }
467    .into())
468}
469
470// Extract full type and first generic type argument
471// e.g., `EventData<AccountOpened, true>` -> (full type, AccountOpened)
472fn extract_type_with_first_generic(arg: &FnArg) -> syn::Result<(&Type, &TypePath)> {
473    let FnArg::Typed(PatType { ty, .. }) = arg else {
474        return Err(Error::new_spanned(arg, "expected typed argument"));
475    };
476
477    let Type::Path(type_path) = ty.as_ref() else {
478        return Err(Error::new_spanned(ty, "expected path type with generic"));
479    };
480
481    let segment = type_path
482        .path
483        .segments
484        .last()
485        .ok_or_else(|| Error::new_spanned(type_path, "empty type path"))?;
486
487    let PathArguments::AngleBracketed(args) = &segment.arguments else {
488        return Err(Error::new_spanned(
489            segment,
490            format!("expected generic arguments on {}", segment.ident),
491        ));
492    };
493
494    // Find first Type::Path argument
495    let inner = args
496        .args
497        .iter()
498        .find_map(|arg| match arg {
499            GenericArgument::Type(Type::Path(p)) => Some(p),
500            _ => None,
501        })
502        .ok_or_else(|| Error::new_spanned(args, "expected type argument"))?;
503
504    Ok((ty.as_ref(), inner))
505}
506
507// Extract `AccountBalanceView` from `Action<'_, AccountBalanceView, E>`
508fn extract_projection_type(arg: &FnArg) -> syn::Result<&TypePath> {
509    let FnArg::Typed(PatType { ty, .. }) = arg else {
510        return Err(Error::new_spanned(arg, "expected typed argument"));
511    };
512
513    let Type::Path(type_path) = ty.as_ref() else {
514        return Err(Error::new_spanned(
515            ty,
516            "expected path type like Action<'_, P, E>",
517        ));
518    };
519
520    let segment = type_path
521        .path
522        .segments
523        .last()
524        .ok_or_else(|| Error::new_spanned(type_path, "empty type path"))?;
525
526    if segment.ident != "Action" {
527        return Err(Error::new_spanned(
528            segment,
529            format!("expected Action<'_, P, E>, found {}", segment.ident),
530        ));
531    }
532
533    let PathArguments::AngleBracketed(args) = &segment.arguments else {
534        return Err(Error::new_spanned(
535            segment,
536            "expected generic arguments: Action<'_, P, E>",
537        ));
538    };
539
540    // Find first Type argument (skip lifetime)
541    let inner = args
542        .args
543        .iter()
544        .find_map(|arg| match arg {
545            GenericArgument::Type(Type::Path(p)) => Some(p),
546            _ => None,
547        })
548        .ok_or_else(|| Error::new_spanned(args, "expected projection type in Action<'_, P, E>"))?;
549
550    Ok(inner)
551}
552
553/// Implements the `Snapshot` trait for projection state restoration.
554///
555/// This macro takes an async function that restores a projection from a snapshot
556/// and generates the `Snapshot` trait implementation.
557///
558/// # Function Signature
559///
560/// The function must have this signature:
561///
562/// ```rust,ignore
563/// async fn restore(
564///     context: &evento::context::RwContext,
565///     id: String,
566/// ) -> anyhow::Result<Option<LoadResult<ProjectionType>>>
567/// ```
568///
569/// # Return Value
570///
571/// - `Ok(Some(LoadResult { ... }))` - Snapshot found, restore from this state
572/// - `Ok(None)` - No snapshot, rebuild from events
573/// - `Err(...)` - Error during restoration
574///
575/// # Example
576///
577/// ```rust,ignore
578/// #[evento::snapshot]
579/// async fn restore(
580///     context: &evento::context::RwContext,
581///     id: String,
582/// ) -> anyhow::Result<Option<LoadResult<AccountBalanceView>>> {
583///     // Query snapshot from database
584///     let snapshot = context.read()
585///         .query_snapshot::<AccountBalanceView>(&id)
586///         .await?;
587///
588///     Ok(snapshot)
589/// }
590/// ```
591///
592/// # Generated Code
593///
594/// The macro generates a `Snapshot` trait implementation for the projection type
595/// extracted from the return type's `LoadResult<T>`.
596#[proc_macro_attribute]
597pub fn snapshot(_attr: TokenStream, item: TokenStream) -> TokenStream {
598    let input = parse_macro_input!(item as ItemFn);
599
600    match snapshot_impl(&input, false) {
601        Ok(tokens) => tokens,
602        Err(e) => e.to_compile_error().into(),
603    }
604}
605
606/// Debug variant of [`snapshot`] that writes generated code to a file.
607///
608/// The generated code is written to `target/evento_debug_snapshot_macro.rs`
609/// for inspection. Useful for understanding what the macro produces.
610///
611/// # Example
612///
613/// ```rust,ignore
614/// #[evento::debug_snapshot]
615/// async fn restore(
616///     context: &evento::context::RwContext,
617///     id: String,
618/// ) -> anyhow::Result<Option<LoadResult<MyView>>> {
619///     Ok(None)
620/// }
621/// ```
622#[proc_macro_attribute]
623pub fn debug_snapshot(_attr: TokenStream, item: TokenStream) -> TokenStream {
624    let input = parse_macro_input!(item as ItemFn);
625
626    match snapshot_impl(&input, true) {
627        Ok(tokens) => tokens,
628        Err(e) => e.to_compile_error().into(),
629    }
630}
631
632fn snapshot_impl(input: &ItemFn, debug: bool) -> syn::Result<TokenStream> {
633    let fn_name = &input.sig.ident;
634
635    // Extract return type: anyhow::Result<Option<AccountBalanceView>>
636    let return_type = match &input.sig.output {
637        ReturnType::Type(_, ty) => ty,
638        ReturnType::Default => {
639            return Err(Error::new_spanned(
640                &input.sig,
641                "expected return type: anyhow::Result<Option<T>>",
642            ));
643        }
644    };
645
646    // Extract the inner type (AccountBalanceView) from Result<Option<T>>
647    // let projection_type = extract_result_option_inner(return_type)?;
648
649    // Level 1: Result<...>
650    let option_type = extract_generic_inner(return_type, "Result")?;
651
652    // Level 2: Option<...>
653    let load_result_type = extract_generic_inner(option_type, "Option")?;
654
655    // Level 3: LoadResult<T>
656    let projection_type = extract_generic_inner(load_result_type, "LoadResult")?;
657
658    let output = quote! {
659        #input
660
661        impl ::evento::projection::Snapshot for #projection_type {
662            fn restore<'a>(
663                context: &'a ::evento::context::RwContext,
664                id: String,
665            ) -> ::std::pin::Pin<Box<dyn ::std::future::Future<Output = ::anyhow::Result<Option<evento::LoadResult<Self>>>> + Send + 'a>> {
666                Box::pin(async move { #fn_name(context, id).await })
667            }
668        }
669    };
670
671    if !debug {
672        return Ok(output.into());
673    }
674
675    let manifest_dir = env!("CARGO_MANIFEST_DIR");
676    let debug_path =
677        std::path::PathBuf::from(&manifest_dir).join("../target/evento_debug_snapshot_macro.rs"); // adjust ../ as needed
678
679    std::fs::write(&debug_path, output.to_string()).ok();
680
681    let debug_path_str = debug_path
682        .canonicalize()
683        .unwrap()
684        .to_string_lossy()
685        .to_string();
686
687    Ok(quote! {
688        include!(#debug_path_str);
689    }
690    .into())
691}
692
693// Extract inner type from Wrapper<T>
694fn extract_generic_inner<'a>(ty: &'a Type, expected: &str) -> syn::Result<&'a Type> {
695    let Type::Path(type_path) = ty else {
696        return Err(Error::new_spanned(ty, format!("expected {}<T>", expected)));
697    };
698
699    let segment = type_path
700        .path
701        .segments
702        .last()
703        .ok_or_else(|| Error::new_spanned(type_path, "empty type path"))?;
704
705    if segment.ident != expected {
706        return Err(Error::new_spanned(
707            segment,
708            format!("expected {}, found {}", expected, segment.ident),
709        ));
710    }
711
712    let PathArguments::AngleBracketed(args) = &segment.arguments else {
713        return Err(Error::new_spanned(
714            segment,
715            format!("expected {}<T>", expected),
716        ));
717    };
718
719    args.args
720        .iter()
721        .find_map(|arg| match arg {
722            GenericArgument::Type(t) => Some(t),
723            _ => None,
724        })
725        .ok_or_else(|| Error::new_spanned(args, format!("expected type in {}<T>", expected)))
726}