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}