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 snapshot;
12mod type_utils;
13
14use proc_macro::TokenStream;
15use syn::parse_macro_input;
16
17#[proc_macro_derive(EsEvent, attributes(es_event))]
18pub fn es_event_derive(input: TokenStream) -> TokenStream {
19    let ast = parse_macro_input!(input as syn::DeriveInput);
20    match event::derive(ast) {
21        Ok(tokens) => tokens.into(),
22        Err(e) => e.write_errors().into(),
23    }
24}
25
26/// Automatically captures function arguments into the event context.
27///
28/// This attribute macro wraps functions to automatically insert specified arguments
29/// into the current `EventContext` (`es_entity::context::EventContext`), making them
30/// available for audit trails when events are persisted.
31///
32/// # Behavior
33///
34/// - **For async functions**: Uses the `WithEventContext` trait
35///   (`es_entity::context::WithEventContext`) to propagate context across async boundaries
36/// - **For sync functions**: Uses `EventContext::fork()` (`es_entity::context::EventContext::fork`)
37///   to create an isolated child context
38///
39/// # Syntax
40///
41/// ```rust,ignore
42/// #[es_event_context]              // No arguments captured
43/// #[es_event_context(arg1)]         // Capture single argument
44/// #[es_event_context(arg1, arg2)]   // Capture multiple arguments
45/// ```
46///
47/// # Examples
48///
49/// ## Async function with argument capture
50/// ```rust,ignore
51/// use es_entity_macros::es_event_context;
52///
53/// impl UserService {
54///     #[es_event_context(user_id, operation)]
55///     async fn update_user(&self, user_id: UserId, operation: &str, data: UserData) -> Result<()> {
56///         // user_id and operation are automatically added to context
57///         // They will be included when events are persisted
58///         self.repo.update(data).await
59///     }
60/// }
61/// ```
62///
63/// ## Sync function with context isolation
64/// ```rust,ignore
65/// use es_entity_macros::es_event_context;
66///
67/// impl Calculator {
68///     #[es_event_context(transaction_id)]
69///     fn process(&mut self, transaction_id: u64, amount: i64) {
70///         // transaction_id is captured in an isolated context
71///         // Parent context is restored when function exits
72///         self.apply_transaction(amount);
73///     }
74/// }
75/// ```
76///
77/// ## Manual context additions
78/// ```rust,ignore
79/// use es_entity_macros::es_event_context;
80/// use es_entity::context::EventContext;
81///
82/// #[es_event_context(request_id)]
83/// async fn handle_request(request_id: String, data: RequestData) {
84///     // request_id is automatically captured
85///     
86///     // You can still manually add more context
87///     let mut ctx = EventContext::current();
88///     ctx.insert("timestamp", &chrono::Utc::now()).unwrap();
89///     
90///     process_data(data).await;
91/// }
92/// ```
93///
94/// # Context Keys
95///
96/// Arguments are captured using their parameter names as keys. For example,
97/// `user_id: UserId` will be stored with key `"user_id"` in the context.
98///
99/// # See Also
100///
101/// - `EventContext` (`es_entity::context::EventContext`) - The context management system
102/// - `WithEventContext` (`es_entity::context::WithEventContext`) - Async context propagation
103/// - Event Context chapter in the book for complete usage patterns
104#[proc_macro_attribute]
105pub fn es_event_context(args: TokenStream, input: TokenStream) -> TokenStream {
106    let ast = parse_macro_input!(input as syn::ItemFn);
107    match es_event_context::make(args, ast) {
108        Ok(tokens) => tokens.into(),
109        Err(e) => e.write_errors().into(),
110    }
111}
112
113#[proc_macro_derive(EsEntity, attributes(es_entity))]
114pub fn es_entity_derive(input: TokenStream) -> TokenStream {
115    let ast = parse_macro_input!(input as syn::DeriveInput);
116    match entity::derive(ast) {
117        Ok(tokens) => tokens.into(),
118        Err(e) => e.write_errors().into(),
119    }
120}
121
122#[proc_macro_derive(EsSnapshot, attributes(es_snapshot))]
123pub fn es_snapshot_derive(input: TokenStream) -> TokenStream {
124    let ast = parse_macro_input!(input as syn::DeriveInput);
125    match snapshot::derive(ast) {
126        Ok(tokens) => tokens.into(),
127        Err(e) => e.write_errors().into(),
128    }
129}
130
131#[proc_macro_derive(EsRepo, attributes(es_repo))]
132pub fn es_repo_derive(input: TokenStream) -> TokenStream {
133    let ast = parse_macro_input!(input as syn::DeriveInput);
134    match repo::derive(ast) {
135        Ok(tokens) => tokens.into(),
136        Err(e) => e.write_errors().into(),
137    }
138}
139
140#[proc_macro]
141#[doc(hidden)]
142pub fn expand_es_query(input: TokenStream) -> TokenStream {
143    let input = parse_macro_input!(input as query::QueryInput);
144    match query::expand(input) {
145        Ok(tokens) => tokens.into(),
146        Err(e) => e.write_errors().into(),
147    }
148}
149
150/// Diagnostic accessors for the final, composed repository rejection enum.
151/// `Display`/`Error` come from the composed enum's own `errlanes::Rejection`
152/// derive (via each variant's `#[error("{0}")]`/`#[source]`), not from here.
153#[proc_macro_derive(ConstraintRejection)]
154pub fn constraint_rejection(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
155    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
156    let ident = ast.ident;
157    let syn::Data::Enum(data) = ast.data else {
158        return quote::quote!(compile_error!("expected constraint enum");).into();
159    };
160    let variants: Vec<_> = data.variants.iter().map(|v| &v.ident).collect();
161    quote::quote! {
162        impl #ident {
163            pub fn diagnostics(&self) -> &es_entity::ConstraintDiagnostics {
164                match self { #(Self::#variants(conflict) => &conflict.diagnostics),* }
165            }
166            pub fn constraint_name(&self) -> &str { self.diagnostics().constraint }
167            pub fn kind(&self) -> es_entity::ConstraintKind { self.diagnostics().kind }
168            pub fn is_unique(&self) -> bool { self.kind() == es_entity::ConstraintKind::Unique }
169            pub fn is_foreign_key(&self) -> bool { self.kind() == es_entity::ConstraintKind::ForeignKey }
170            pub fn is_check(&self) -> bool { self.kind() == es_entity::ConstraintKind::Check }
171        }
172    }.into()
173}