Skip to main content

es_entity_macros/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
3#![forbid(unsafe_code)]
4
5mod entity;
6mod es_event_context;
7mod event;
8mod index_catalog;
9mod query;
10mod repo;
11mod retry_on_concurrent_modification;
12mod snapshot;
13mod type_utils;
14
15use proc_macro::TokenStream;
16use syn::parse_macro_input;
17
18#[proc_macro_derive(EsEvent, attributes(es_event))]
19pub fn es_event_derive(input: TokenStream) -> TokenStream {
20    let ast = parse_macro_input!(input as syn::DeriveInput);
21    match event::derive(ast) {
22        Ok(tokens) => tokens.into(),
23        Err(e) => e.write_errors().into(),
24    }
25}
26
27/// Retries the annotated async function when it fails with a concurrent
28/// modification error (or with any error when `any_error = true`).
29///
30/// Attempts are spaced with exponential backoff (25ms, 50ms, 100ms, ...
31/// capped at 1s) so a contended entity is not hammered in a hot loop while
32/// the conflicting writer finishes its transaction.
33///
34/// # Arguments
35///
36/// - `max_retries = N` — maximum attempts (default: 3)
37/// - `any_error = true|false` — retry on any error, not just concurrent
38///   modifications (default: false). Only enable this when the annotated
39///   function is fully idempotent.
40#[proc_macro_attribute]
41pub fn retry_on_concurrent_modification(args: TokenStream, input: TokenStream) -> TokenStream {
42    let ast = parse_macro_input!(input as syn::ItemFn);
43    match retry_on_concurrent_modification::make(args, ast) {
44        Ok(tokens) => tokens.into(),
45        Err(e) => e.write_errors().into(),
46    }
47}
48
49/// Automatically captures function arguments into the event context.
50///
51/// This attribute macro wraps functions to automatically insert specified arguments
52/// into the current `EventContext` (`es_entity::context::EventContext`), making them
53/// available for audit trails when events are persisted.
54///
55/// # Behavior
56///
57/// - **For async functions**: Uses the `WithEventContext` trait
58///   (`es_entity::context::WithEventContext`) to propagate context across async boundaries
59/// - **For sync functions**: Uses `EventContext::fork()` (`es_entity::context::EventContext::fork`)
60///   to create an isolated child context
61///
62/// # Syntax
63///
64/// ```rust,ignore
65/// #[es_event_context]              // No arguments captured
66/// #[es_event_context(arg1)]         // Capture single argument
67/// #[es_event_context(arg1, arg2)]   // Capture multiple arguments
68/// ```
69///
70/// # Examples
71///
72/// ## Async function with argument capture
73/// ```rust,ignore
74/// use es_entity_macros::es_event_context;
75///
76/// impl UserService {
77///     #[es_event_context(user_id, operation)]
78///     async fn update_user(&self, user_id: UserId, operation: &str, data: UserData) -> Result<()> {
79///         // user_id and operation are automatically added to context
80///         // They will be included when events are persisted
81///         self.repo.update(data).await
82///     }
83/// }
84/// ```
85///
86/// ## Sync function with context isolation
87/// ```rust,ignore
88/// use es_entity_macros::es_event_context;
89///
90/// impl Calculator {
91///     #[es_event_context(transaction_id)]
92///     fn process(&mut self, transaction_id: u64, amount: i64) {
93///         // transaction_id is captured in an isolated context
94///         // Parent context is restored when function exits
95///         self.apply_transaction(amount);
96///     }
97/// }
98/// ```
99///
100/// ## Manual context additions
101/// ```rust,ignore
102/// use es_entity_macros::es_event_context;
103/// use es_entity::context::EventContext;
104///
105/// #[es_event_context(request_id)]
106/// async fn handle_request(request_id: String, data: RequestData) {
107///     // request_id is automatically captured
108///     
109///     // You can still manually add more context
110///     let mut ctx = EventContext::current();
111///     ctx.insert("timestamp", &chrono::Utc::now()).unwrap();
112///     
113///     process_data(data).await;
114/// }
115/// ```
116///
117/// # Context Keys
118///
119/// Arguments are captured using their parameter names as keys. For example,
120/// `user_id: UserId` will be stored with key `"user_id"` in the context.
121///
122/// # See Also
123///
124/// - `EventContext` (`es_entity::context::EventContext`) - The context management system
125/// - `WithEventContext` (`es_entity::context::WithEventContext`) - Async context propagation
126/// - Event Context chapter in the book for complete usage patterns
127#[proc_macro_attribute]
128pub fn es_event_context(args: TokenStream, input: TokenStream) -> TokenStream {
129    let ast = parse_macro_input!(input as syn::ItemFn);
130    match es_event_context::make(args, ast) {
131        Ok(tokens) => tokens.into(),
132        Err(e) => e.write_errors().into(),
133    }
134}
135
136#[proc_macro_derive(EsEntity, attributes(es_entity))]
137pub fn es_entity_derive(input: TokenStream) -> TokenStream {
138    let ast = parse_macro_input!(input as syn::DeriveInput);
139    match entity::derive(ast) {
140        Ok(tokens) => tokens.into(),
141        Err(e) => e.write_errors().into(),
142    }
143}
144
145#[proc_macro_derive(EsSnapshot, attributes(es_snapshot))]
146pub fn es_snapshot_derive(input: TokenStream) -> TokenStream {
147    let ast = parse_macro_input!(input as syn::DeriveInput);
148    match snapshot::derive(ast) {
149        Ok(tokens) => tokens.into(),
150        Err(e) => e.write_errors().into(),
151    }
152}
153
154#[proc_macro_derive(EsRepo, attributes(es_repo))]
155pub fn es_repo_derive(input: TokenStream) -> TokenStream {
156    let ast = parse_macro_input!(input as syn::DeriveInput);
157    match repo::derive(ast) {
158        Ok(tokens) => tokens.into(),
159        Err(e) => e.write_errors().into(),
160    }
161}
162
163#[proc_macro]
164#[doc(hidden)]
165pub fn expand_es_query(input: TokenStream) -> TokenStream {
166    let input = parse_macro_input!(input as query::QueryInput);
167    match query::expand(input) {
168        Ok(tokens) => tokens.into(),
169        Err(e) => e.write_errors().into(),
170    }
171}