es_entity/macros.rs
1/// Prevent duplicate event processing by checking for idempotent operations.
2///
3/// Guards against replaying the same mutation in event-sourced systems.
4/// Returns [`AlreadyApplied`][crate::Idempotent::AlreadyApplied] early if matching events are found, allowing the caller
5/// to skip redundant operations. Use `resets_on` to allow re-applying after an intervening event.
6///
7/// # Parameters
8///
9/// - `$events`: Event collection to search (usually chronologically reversed)
10/// - `already_applied:` One or more event patterns that indicate the operation was already applied.
11/// Multiple patterns are supported — each is checked independently.
12/// - `resets_on:` Optional event pattern that resets the guard, allowing re-execution.
13/// Use Rust's native or-pattern (`P1 | P2`) to match multiple reset events.
14///
15/// When iterating events in reverse, if a `resets_on` event is found before the
16/// `already_applied` event, the guard allows re-execution. This is useful for
17/// toggle-like operations (freeze/unfreeze) or when a state change should
18/// invalidate a previous idempotency check.
19///
20/// # Examples
21///
22/// ```rust
23/// use es_entity::{idempotency_guard, Idempotent};
24/// pub enum UserEvent{
25/// Initialized {id: u64, name: String},
26/// NameUpdated {name: String}
27/// }
28///
29/// pub struct User{
30/// events: Vec<UserEvent>
31/// }
32///
33/// impl User{
34/// pub fn update_name(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
35/// let name = new_name.into();
36/// idempotency_guard!(
37/// self.events.iter().rev(),
38/// already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name
39/// );
40/// self.events.push(UserEvent::NameUpdated{name});
41/// Idempotent::Executed(())
42/// }
43///
44/// pub fn update_name_resettable(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
45/// let name = new_name.into();
46/// idempotency_guard!(
47/// self.events.iter().rev(),
48/// already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name,
49/// resets_on: UserEvent::NameUpdated {..}
50/// // if any other NameUpdated happened more recently, allow re-applying
51/// );
52/// self.events.push(UserEvent::NameUpdated{name});
53/// Idempotent::Executed(())
54/// }
55/// }
56///
57/// let mut user1 = User{ events: vec![] };
58/// let mut user2 = User{ events: vec![] };
59/// assert!(user1.update_name("Alice").did_execute());
60/// // updating "Alice" again ignored because same event with same name exists
61/// assert!(user1.update_name("Alice").was_already_applied());
62///
63/// assert!(user2.update_name_resettable("Alice").did_execute());
64/// assert!(user2.update_name_resettable("Bob").did_execute());
65/// // updating "Alice" again works because Bob's NameUpdated resets the guard
66/// assert!(user2.update_name_resettable("Alice").did_execute());
67/// ```
68///
69/// ## Multiple `already_applied` patterns
70///
71/// ```rust
72/// use es_entity::{idempotency_guard, Idempotent};
73///
74/// pub enum ConfigEvent {
75/// Initialized { id: u64 },
76/// Updated { key: String, value: String },
77/// KeyRotated { key: String },
78/// }
79///
80/// pub struct Config {
81/// events: Vec<ConfigEvent>,
82/// }
83///
84/// impl Config {
85/// pub fn apply_change(&mut self, key: String, value: String) -> Idempotent<()> {
86/// idempotency_guard!(
87/// self.events.iter().rev(),
88/// already_applied: ConfigEvent::Updated { key: k, value: v } if k == &key && v == &value,
89/// already_applied: ConfigEvent::KeyRotated { key: k } if k == &key,
90/// resets_on: ConfigEvent::Initialized { .. }
91/// );
92/// self.events.push(ConfigEvent::Updated { key, value });
93/// Idempotent::Executed(())
94/// }
95/// }
96///
97/// let mut config = Config { events: vec![] };
98/// assert!(config.apply_change("k".into(), "v".into()).did_execute());
99/// assert!(config.apply_change("k".into(), "v".into()).was_already_applied());
100/// ```
101#[macro_export]
102macro_rules! idempotency_guard {
103 // already_applied + resets_on (must come before already_applied-only to avoid ambiguity)
104 ($events:expr,
105 $(already_applied: $pattern:pat $(if $guard:expr)? ,)+
106 resets_on: $break_pattern:pat $(if $break_guard:expr)? $(,)?) => {
107 for event in $events {
108 match event {
109 $(
110 $pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
111 )+
112 $break_pattern $(if $break_guard)? => break,
113 _ => {}
114 }
115 }
116 };
117 // already_applied only
118 ($events:expr,
119 $(already_applied: $pattern:pat $(if $guard:expr)?),+ $(,)?) => {
120 for event in $events {
121 match event {
122 $(
123 $pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
124 )+
125 _ => {}
126 }
127 }
128 };
129}
130
131/// Execute an event-sourced query with automatic entity hydration.
132///
133/// Executes user-defined queries and returns entities by internally
134/// joining with events table to hydrate entities, essentially giving the
135/// illusion of working with just the index table.
136///
137/// **Important**: This macro only works inside functions (`fn`) that are defined
138/// within structs that have `#[derive(EsRepo)]` applied. The macro relies on
139/// the repository context to properly hydrate entities.
140///
141/// # Returns
142///
143/// Returns an [`EsQuery`](crate::query::EsQuery) struct that provides methods
144/// like [`fetch_optional()`](crate::query::EsQuery::fetch_optional) and
145/// [`fetch_n()`](crate::query::EsQuery::fetch_n) for executing the
146/// query and retrieving hydrated entities.
147///
148/// # Parameters
149///
150/// - `tbl_prefix`: Table prefix to ignore when deriving entity names from table names (optional)
151/// - `entity`: Override the entity type (optional, useful when table name doesn't match entity name)
152/// - SQL query string
153/// - Additional arguments for the SQL query (optional)
154///
155/// # Examples
156/// ```ignore
157/// // Basic usage
158/// es_query!("SELECT id FROM users WHERE id = $1", id)
159///
160/// // With table prefix
161/// es_query!(
162/// tbl_prefix = "app",
163/// "SELECT id FROM app_users WHERE active = true"
164/// )
165///
166/// // With custom entity type
167/// es_query!(
168/// entity = User,
169/// "SELECT id FROM custom_users_table WHERE id = $1",
170/// id as UserId
171/// )
172/// ```
173#[macro_export]
174macro_rules! es_query {
175 // With entity override
176 (
177 entity = $entity:ident,
178 $query:expr,
179 $($args:tt)*
180 ) => ({
181 $crate::expand_es_query!(
182 entity = $entity,
183 sql = $query,
184 args = [$($args)*]
185 )
186 });
187 // With entity override - no args
188 (
189 entity = $entity:ident,
190 $query:expr
191 ) => ({
192 $crate::expand_es_query!(
193 entity = $entity,
194 sql = $query
195 )
196 });
197
198 // With tbl_prefix
199 (
200 tbl_prefix = $tbl_prefix:literal,
201 $query:expr,
202 $($args:tt)*
203 ) => ({
204 $crate::expand_es_query!(
205 tbl_prefix = $tbl_prefix,
206 sql = $query,
207 args = [$($args)*]
208 )
209 });
210 // With tbl_prefix - no args
211 (
212 tbl_prefix = $tbl_prefix:literal,
213 $query:expr
214 ) => ({
215 $crate::expand_es_query!(
216 tbl_prefix = $tbl_prefix,
217 sql = $query
218 )
219 });
220
221 // Basic form
222 (
223 $query:expr,
224 $($args:tt)*
225 ) => ({
226 $crate::expand_es_query!(
227 sql = $query,
228 args = [$($args)*]
229 )
230 });
231 // Basic form - no args
232 (
233 $query:expr
234 ) => ({
235 $crate::expand_es_query!(
236 sql = $query
237 )
238 });
239}
240
241// Helper macro for common entity_id implementations (internal use only)
242#[doc(hidden)]
243#[macro_export]
244macro_rules! __entity_id_common_impls {
245 ($name:ident) => {
246 impl $name {
247 #[allow(clippy::new_without_default)]
248 pub fn new() -> Self {
249 $crate::prelude::uuid::Uuid::now_v7().into()
250 }
251 }
252
253 impl From<$crate::prelude::uuid::Uuid> for $name {
254 fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
255 Self(uuid)
256 }
257 }
258
259 impl From<$name> for $crate::prelude::uuid::Uuid {
260 fn from(id: $name) -> Self {
261 id.0
262 }
263 }
264
265 impl From<&$name> for $crate::prelude::uuid::Uuid {
266 fn from(id: &$name) -> Self {
267 id.0
268 }
269 }
270
271 impl std::fmt::Display for $name {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 write!(f, "{}", self.0)
274 }
275 }
276
277 impl std::str::FromStr for $name {
278 type Err = $crate::prelude::uuid::Error;
279
280 fn from_str(s: &str) -> Result<Self, Self::Err> {
281 Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
282 }
283 }
284 };
285}
286
287// Helper macro for GraphQL-specific entity_id implementations (internal use only)
288#[doc(hidden)]
289#[macro_export]
290macro_rules! __entity_id_graphql_impls {
291 ($name:ident) => {
292 impl From<$crate::graphql::UUID> for $name {
293 fn from(id: $crate::graphql::UUID) -> Self {
294 $name($crate::prelude::uuid::Uuid::from(&id))
295 }
296 }
297
298 impl From<&$crate::graphql::UUID> for $name {
299 fn from(id: &$crate::graphql::UUID) -> Self {
300 $name($crate::prelude::uuid::Uuid::from(id))
301 }
302 }
303 };
304}
305
306// Helper macro for additional conversions (internal use only)
307#[doc(hidden)]
308#[macro_export]
309macro_rules! __entity_id_conversions {
310 ($($from:ty => $to:ty),* $(,)?) => {
311 $(
312 impl From<$from> for $to {
313 fn from(id: $from) -> Self {
314 <$to>::from($crate::prelude::uuid::Uuid::from(id))
315 }
316 }
317 impl From<$to> for $from {
318 fn from(id: $to) -> Self {
319 <$from>::from($crate::prelude::uuid::Uuid::from(id))
320 }
321 }
322 )*
323 };
324}
325
326#[doc(hidden)]
327#[cfg(all(feature = "graphql", feature = "json-schema"))]
328#[macro_export]
329macro_rules! entity_id {
330 // Match identifiers without conversions
331 ($($name:ident),+ $(,)?) => {
332 $crate::entity_id! { $($name),+ ; }
333 };
334 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
335 $(
336 #[derive(
337 $crate::prelude::sqlx::Type,
338 Debug,
339 Clone,
340 Copy,
341 PartialEq,
342 Eq,
343 PartialOrd,
344 Ord,
345 Hash,
346 $crate::prelude::serde::Deserialize,
347 $crate::prelude::serde::Serialize,
348 $crate::prelude::schemars::JsonSchema,
349 )]
350 #[schemars(crate = "es_entity::prelude::schemars")]
351 #[serde(crate = "es_entity::prelude::serde")]
352 #[serde(transparent)]
353 #[sqlx(transparent)]
354 pub struct $name($crate::prelude::uuid::Uuid);
355 $crate::__entity_id_common_impls!($name);
356 $crate::__entity_id_graphql_impls!($name);
357 )+
358 $crate::__entity_id_conversions!($($from => $to),*);
359 };
360}
361
362#[doc(hidden)]
363#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
364#[macro_export]
365macro_rules! entity_id {
366 // Match identifiers without conversions
367 ($($name:ident),+ $(,)?) => {
368 $crate::entity_id! { $($name),+ ; }
369 };
370 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
371 $(
372 #[derive(
373 $crate::prelude::sqlx::Type,
374 Debug,
375 Clone,
376 Copy,
377 PartialEq,
378 Eq,
379 PartialOrd,
380 Ord,
381 Hash,
382 $crate::prelude::serde::Deserialize,
383 $crate::prelude::serde::Serialize,
384 )]
385 #[serde(crate = "es_entity::prelude::serde")]
386 #[serde(transparent)]
387 #[sqlx(transparent)]
388 pub struct $name($crate::prelude::uuid::Uuid);
389 $crate::__entity_id_common_impls!($name);
390 $crate::__entity_id_graphql_impls!($name);
391 )+
392 $crate::__entity_id_conversions!($($from => $to),*);
393 };
394}
395
396#[doc(hidden)]
397#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
398#[macro_export]
399macro_rules! entity_id {
400 // Match identifiers without conversions
401 ($($name:ident),+ $(,)?) => {
402 $crate::entity_id! { $($name),+ ; }
403 };
404 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
405 $(
406 #[derive(
407 $crate::prelude::sqlx::Type,
408 Debug,
409 Clone,
410 Copy,
411 PartialEq,
412 Eq,
413 PartialOrd,
414 Ord,
415 Hash,
416 $crate::prelude::serde::Deserialize,
417 $crate::prelude::serde::Serialize,
418 $crate::prelude::schemars::JsonSchema,
419 )]
420 #[schemars(crate = "es_entity::prelude::schemars")]
421 #[serde(crate = "es_entity::prelude::serde")]
422 #[serde(transparent)]
423 #[sqlx(transparent)]
424 pub struct $name($crate::prelude::uuid::Uuid);
425 $crate::__entity_id_common_impls!($name);
426 )+
427 $crate::__entity_id_conversions!($($from => $to),*);
428 };
429}
430
431/// Create UUID-wrappers for database operations.
432///
433/// This macro generates type-safe UUID-wrapper structs with trait support for
434/// serialization, database operations, GraphQL integration, and JSON schema generation.
435///
436/// # Features
437///
438/// The macro automatically includes different trait implementations based on enabled features:
439/// - `graphql`: Adds GraphQL UUID conversion traits
440/// - `json-schema`: Adds JSON schema generation support
441///
442/// # Generated Traits
443///
444/// All entity IDs automatically implement:
445/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
446/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
447/// - `sqlx::Type` (with transparent database type)
448/// - `Display` and `FromStr` for string conversion
449/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
450///
451/// # Parameters
452///
453/// - `$name`: One or more entity ID type names to create
454/// - `$from => $to`: Optional conversion pairs between different entity ID types
455///
456/// # Examples
457///
458/// ```rust
459/// use es_entity::entity_id;
460///
461/// entity_id! { UserId, OrderId }
462///
463/// // Creates:
464/// // pub struct UserId(Uuid);
465/// // pub struct OrderId(Uuid);
466/// ```
467///
468/// ```rust
469/// use es_entity::entity_id;
470///
471/// entity_id! {
472/// UserId,
473/// AdminUserId;
474/// UserId => AdminUserId
475/// }
476///
477/// // Creates UserId and AdminUserId with `impl From` conversion between them
478/// ```
479#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
480#[macro_export]
481macro_rules! entity_id {
482 // Match identifiers without conversions
483 ($($name:ident),+ $(,)?) => {
484 $crate::entity_id! { $($name),+ ; }
485 };
486 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
487 $(
488 #[derive(
489 $crate::prelude::sqlx::Type,
490 Debug,
491 Clone,
492 Copy,
493 PartialEq,
494 Eq,
495 PartialOrd,
496 Ord,
497 Hash,
498 $crate::prelude::serde::Deserialize,
499 $crate::prelude::serde::Serialize,
500 )]
501 #[serde(crate = "es_entity::prelude::serde")]
502 #[serde(transparent)]
503 #[sqlx(transparent)]
504 pub struct $name($crate::prelude::uuid::Uuid);
505 $crate::__entity_id_common_impls!($name);
506 )+
507 $crate::__entity_id_conversions!($($from => $to),*);
508 };
509}