evento_macro/lib.rs
1//! # Evento Macros
2//!
3//! This crate provides procedural macros for the Evento event sourcing framework.
4//! These macros simplify the implementation of aggregators and event handlers by
5//! generating boilerplate code automatically.
6//!
7//! ## Macros
8//!
9//! - [`aggregator`] - Implements the [`Aggregator`] trait for structs with event handler methods
10//! - [`handler`] - Creates event handler functions for use with subscriptions
11//! - [`AggregatorName`] - Derives the [`AggregatorName`] trait for event types
12//!
13//! ## Usage
14//!
15//! This crate is typically used through the main `evento` crate with the `macro` feature enabled:
16//!
17//! ```toml
18//! [dependencies]
19//! evento = { version = "1.0", features = ["macro"] }
20//! ```
21//!
22//! ## Examples
23//!
24//! See the individual macro documentation for detailed usage examples.
25
26use convert_case::{Case, Casing};
27use proc_macro::TokenStream;
28use quote::{quote, ToTokens};
29use sha3::{Digest, Sha3_256};
30use std::ops::Deref;
31use syn::{parse_macro_input, spanned::Spanned, Ident, ItemFn, ItemImpl, ItemStruct};
32
33/// Creates an event handler for use with event subscriptions
34///
35/// The `#[evento::handler(AggregateType)]` attribute macro transforms a function into an event handler
36/// that can be used with [`evento::subscribe`]. The macro generates the necessary boilerplate to
37/// integrate with the subscription system.
38///
39/// # Syntax
40///
41/// ```ignore
42/// #[evento::handler(AggregateType)]
43/// async fn handler_name<E: evento::Executor>(
44/// context: &evento::Context<'_, E>,
45/// event: EventDetails<EventType>,
46/// ) -> anyhow::Result<()> {
47/// // Handler logic
48/// Ok(())
49/// }
50/// ```
51///
52/// # Parameters
53///
54/// - `AggregateType`: The aggregate type this handler is associated with
55///
56/// # Function Requirements
57///
58/// The decorated function must:
59/// - Be `async`
60/// - Take `&evento::Context<'_, E>` as first parameter where `E: evento::Executor`
61/// - Take `EventDetails<SomeEventType>` as second parameter
62/// - Return `anyhow::Result<()>`
63///
64/// # Examples
65///
66/// ```no_run
67/// use evento::{Context, EventDetails, Executor};
68/// use bincode::{Encode, Decode};
69///
70/// # use evento::AggregatorName;
71/// # #[derive(AggregatorName, Encode, Decode)]
72/// # struct UserCreated { name: String }
73/// # #[derive(Default, Encode, Decode, Clone, Debug)]
74/// # struct User;
75/// # #[evento::aggregator]
76/// # impl User {}
77///
78/// #[evento::handler(User)]
79/// async fn on_user_created<E: Executor>(
80/// context: &Context<'_, E>,
81/// event: EventDetails<UserCreated>,
82/// ) -> anyhow::Result<()> {
83/// println!("User created: {}", event.data.name);
84///
85/// // Can trigger side effects, call external services, etc.
86///
87/// Ok(())
88/// }
89///
90/// // Use with subscription
91/// # async fn setup(executor: evento::Sqlite) -> anyhow::Result<()> {
92/// evento::subscribe("user-handlers")
93/// .aggregator::<User>()
94/// .handler(on_user_created())
95/// .run(&executor)
96/// .await?;
97/// # Ok(())
98/// # }
99/// ```
100///
101/// # Generated Code
102///
103/// The macro generates:
104/// - A struct implementing [`evento::SubscribeHandler`]
105/// - A constructor function returning an instance of that struct
106/// - Type-safe event filtering based on the event type
107#[proc_macro_attribute]
108pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream {
109 let item: ItemFn = parse_macro_input!(item);
110 let fn_ident = item.sig.ident.to_owned();
111 let struct_ident = item.sig.ident.to_string().to_case(Case::UpperCamel);
112 let struct_ident = Ident::new(&struct_ident, item.span());
113 let aggregator_ident = Ident::new(&attr.to_string(), item.span());
114 let Some(syn::FnArg::Typed(event_arg)) = item.sig.inputs.get(1) else {
115 return syn::Error::new_spanned(item, "Unable to find event input")
116 .into_compile_error()
117 .into();
118 };
119
120 let syn::Type::Path(ty) = *event_arg.ty.clone() else {
121 return syn::Error::new_spanned(item, "Unable to find event input type")
122 .into_compile_error()
123 .into();
124 };
125
126 let Some(event_arg_segment) = ty.path.segments.first() else {
127 return syn::Error::new_spanned(item, "Unable to find event input type iden")
128 .into_compile_error()
129 .into();
130 };
131
132 let syn::PathArguments::AngleBracketed(ref arguments) = event_arg_segment.arguments else {
133 return syn::Error::new_spanned(item, "Unable to find event input type iden")
134 .into_compile_error()
135 .into();
136 };
137
138 let Some(syn::GenericArgument::Type(syn::Type::Path(ty))) = arguments.args.first() else {
139 return syn::Error::new_spanned(item, "Unable to find event input type iden")
140 .into_compile_error()
141 .into();
142 };
143
144 let Some(segment) = ty.path.segments.first() else {
145 return syn::Error::new_spanned(item, "Unable to find event input type iden")
146 .into_compile_error()
147 .into();
148 };
149
150 let event_ident = segment.ident.to_owned();
151
152 quote! {
153 struct #struct_ident;
154
155 fn #fn_ident() -> #struct_ident { #struct_ident }
156
157 impl<E: evento::Executor> evento::SubscribeHandler<E> for #struct_ident {
158 fn handle<'async_trait>(&'async_trait self, context: &'async_trait evento::Context<'_, E>) -> std::pin::Pin<Box<dyn std::future::Future<Output=anyhow::Result<()>> + Send + 'async_trait>>
159 where
160 Self: Sync + 'async_trait
161 {
162 Box::pin(async move {
163 if let Some(data) = context.event.to_details()? {
164 return Self::#fn_ident(context, data).await;
165 }
166
167 Ok(())
168 })
169 }
170
171 fn aggregator_type(&self) -> &'static str{ #aggregator_ident::name() }
172 fn event_name(&self) -> &'static str { #event_ident::name() }
173 }
174
175 impl #struct_ident {
176 #item
177 }
178 }.into()
179}
180
181/// Implements the [`evento::Aggregator`] trait for structs with event handler methods
182///
183/// The `#[evento::aggregator]` attribute macro automatically implements the [`evento::Aggregator`]
184/// trait by generating an `aggregate` method that dispatches events to the appropriate handler
185/// methods based on event type.
186///
187/// # Syntax
188///
189/// ```ignore
190/// #[evento::aggregator]
191/// impl AggregateStruct {
192/// async fn event_handler_name(&mut self, event: EventDetails<EventType>) -> anyhow::Result<()> {
193/// // Update self based on event
194/// Ok(())
195/// }
196/// }
197/// ```
198///
199/// # Requirements
200///
201/// - The struct must implement all required traits for [`evento::Aggregator`]:
202/// - `Default`, `Send`, `Sync`, `Clone`, `Debug`
203/// - `bincode::Encode`, `bincode::Decode`
204/// - [`evento::AggregatorName`]
205/// - Handler methods must be `async`
206/// - Handler methods must take `&mut self` and `EventDetails<SomeEventType>`
207/// - Handler methods must return `anyhow::Result<()>`
208///
209/// # Event Matching
210///
211/// The macro matches events to handler methods by calling [`evento::Event::to_details`] with
212/// the event type inferred from the handler method's parameter type.
213///
214/// # Examples
215///
216/// ```no_run
217/// use evento::{EventDetails, AggregatorName};
218/// use serde::{Deserialize, Serialize};
219/// use bincode::{Encode, Decode};
220///
221/// #[derive(AggregatorName, Encode, Decode)]
222/// struct UserCreated {
223/// name: String,
224/// email: String,
225/// }
226///
227/// #[derive(AggregatorName, Encode, Decode)]
228/// struct UserEmailChanged {
229/// email: String,
230/// }
231///
232/// #[derive(Default, Serialize, Deserialize, Encode, Decode, Clone, Debug)]
233/// struct User {
234/// name: String,
235/// email: String,
236/// version: u32,
237/// }
238///
239/// #[evento::aggregator]
240/// impl User {
241/// async fn user_created(&mut self, event: EventDetails<UserCreated>) -> anyhow::Result<()> {
242/// self.name = event.data.name;
243/// self.email = event.data.email;
244/// self.version = event.version as u32;
245/// Ok(())
246/// }
247///
248/// async fn user_email_changed(&mut self, event: EventDetails<UserEmailChanged>) -> anyhow::Result<()> {
249/// self.email = event.data.email;
250/// self.version = event.version as u32;
251/// Ok(())
252/// }
253/// }
254/// ```
255///
256/// # Generated Code
257///
258/// The macro generates:
259/// - Implementation of [`evento::Aggregator::aggregate`] with event dispatching
260/// - Implementation of [`evento::Aggregator::revision`] based on a hash of the handler methods
261/// - Implementation of [`evento::AggregatorName`] using the package name and struct name
262#[proc_macro_attribute]
263pub fn aggregator(_attr: TokenStream, item: TokenStream) -> TokenStream {
264 let item: ItemImpl = parse_macro_input!(item);
265 let mut hasher = Sha3_256::new();
266
267 let syn::Type::Path(item_path) = item.self_ty.deref() else {
268 return syn::Error::new_spanned(item, "Unable to find name of impl struct")
269 .into_compile_error()
270 .into();
271 };
272
273 let Some(ident) = item_path.path.get_ident() else {
274 return syn::Error::new_spanned(item, "Unable to get ident of impl struct")
275 .into_compile_error()
276 .into();
277 };
278
279 let handler_fns = item
280 .items
281 .iter()
282 .filter_map(|item| {
283 let syn::ImplItem::Fn(item_fn) = item else {
284 return None;
285 };
286
287 hasher.update(item_fn.to_token_stream().to_string());
288 let ident = item_fn.sig.ident.clone();
289
290 Some(quote! {
291 if let Some(data) = event.to_details()? {
292 self.#ident(data).await?;
293 return Ok(());
294 }
295 })
296 })
297 .collect::<proc_macro2::TokenStream>();
298
299 let revision = format!("{:x}", hasher.finalize());
300 let name = ident.to_string();
301
302 quote! {
303 #item
304
305 impl evento::Aggregator for #ident {
306 fn aggregate<'async_trait>(&'async_trait mut self, event: &'async_trait evento::Event) -> std::pin::Pin<Box<dyn std::future::Future<Output=anyhow::Result<()>> + Send + 'async_trait>>
307 where
308 Self: Sync + 'async_trait
309 {
310 Box::pin(async move {
311 #handler_fns
312
313 Ok(())
314 })
315 }
316
317 fn revision() -> &'static str {
318 #revision
319 }
320 }
321
322 impl evento::AggregatorName for #ident {
323 fn name() -> &'static str {
324 static NAME: std::sync::LazyLock<String> = std::sync::LazyLock::new(||{
325 format!("{}/{}", env!("CARGO_PKG_NAME"), #name)
326 });
327
328 &NAME
329 }
330 }
331 }
332 .into()
333}
334
335/// Derives the [`evento::AggregatorName`] trait for event types
336///
337/// The `#[derive(AggregatorName)]` macro automatically implements the [`evento::AggregatorName`]
338/// trait, which provides a name identifier for the type. This is essential for event types
339/// as it allows the event system to identify and match events by name.
340///
341/// # Usage
342///
343/// Apply this derive macro to structs that represent events:
344///
345/// ```no_run
346/// use evento::AggregatorName;
347/// use bincode::{Encode, Decode};
348///
349/// #[derive(AggregatorName, Encode, Decode)]
350/// struct UserCreated {
351/// name: String,
352/// email: String,
353/// }
354///
355/// // The derived implementation returns the struct name as a string
356/// assert_eq!(UserCreated::name(), "UserCreated");
357/// ```
358///
359/// # Requirements
360///
361/// Event types using this derive must also implement:
362/// - `bincode::Encode` and `bincode::Decode` for serialization
363/// - Usually derived together: `#[derive(AggregatorName, Encode, Decode)]`
364///
365/// # Generated Implementation
366///
367/// The macro generates a simple implementation that returns the struct name as a static string:
368///
369/// ```ignore
370/// impl evento::AggregatorName for YourEventType {
371/// fn name() -> &'static str {
372/// "YourEventType"
373/// }
374/// }
375/// ```
376///
377/// # Event Identification
378///
379/// The name returned by this trait is used by the event system to:
380/// - Match events to their types during deserialization
381/// - Route events to appropriate handlers
382/// - Filter events in subscriptions
383#[proc_macro_derive(AggregatorName)]
384pub fn derive_aggregator_name(input: TokenStream) -> TokenStream {
385 let ItemStruct { ident, .. } = parse_macro_input!(input);
386 let name = ident.to_string();
387
388 quote! {
389 impl evento::AggregatorName for #ident {
390 fn name() -> &'static str {
391 #name
392 }
393 }
394 }
395 .into()
396}