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}