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