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}