Skip to main content

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// When `graphql` feature is enabled, entity IDs become their own GraphQL scalars
289// (e.g. `CustomerId` instead of the generic `UUID`), providing type safety at the API layer.
290#[doc(hidden)]
291#[macro_export]
292macro_rules! __entity_id_graphql_impls {
293    ($name:ident) => {
294        impl From<$crate::graphql::UUID> for $name {
295            fn from(id: $crate::graphql::UUID) -> Self {
296                $name($crate::prelude::uuid::Uuid::from(&id))
297            }
298        }
299
300        impl From<&$crate::graphql::UUID> for $name {
301            fn from(id: &$crate::graphql::UUID) -> Self {
302                $name($crate::prelude::uuid::Uuid::from(id))
303            }
304        }
305
306        $crate::graphql::async_graphql::scalar!($name);
307    };
308}
309
310// Helper macro for additional conversions (internal use only)
311#[doc(hidden)]
312#[macro_export]
313macro_rules! __entity_id_conversions {
314    ($($from:ty => $to:ty),* $(,)?) => {
315        $(
316            impl From<$from> for $to {
317                fn from(id: $from) -> Self {
318                    <$to>::from($crate::prelude::uuid::Uuid::from(id))
319                }
320            }
321            impl From<$to> for $from {
322                fn from(id: $to) -> Self {
323                    <$from>::from($crate::prelude::uuid::Uuid::from(id))
324                }
325            }
326        )*
327    };
328}
329
330#[doc(hidden)]
331#[cfg(all(feature = "graphql", feature = "json-schema"))]
332#[macro_export]
333macro_rules! entity_id {
334    // Match identifiers without conversions
335    ($($name:ident),+ $(,)?) => {
336        $crate::entity_id! { $($name),+ ; }
337    };
338    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
339        $(
340            #[derive(
341                $crate::prelude::sqlx::Type,
342                Debug,
343                Clone,
344                Copy,
345                PartialEq,
346                Eq,
347                PartialOrd,
348                Ord,
349                Hash,
350                $crate::prelude::serde::Deserialize,
351                $crate::prelude::serde::Serialize,
352                $crate::prelude::schemars::JsonSchema,
353            )]
354            #[schemars(crate = "es_entity::prelude::schemars")]
355            #[serde(crate = "es_entity::prelude::serde")]
356            #[serde(transparent)]
357            #[sqlx(transparent)]
358            pub struct $name($crate::prelude::uuid::Uuid);
359            $crate::__entity_id_common_impls!($name);
360            $crate::__entity_id_graphql_impls!($name);
361
362        )+
363        $crate::__entity_id_conversions!($($from => $to),*);
364    };
365}
366
367#[doc(hidden)]
368#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
369#[macro_export]
370macro_rules! entity_id {
371    // Match identifiers without conversions
372    ($($name:ident),+ $(,)?) => {
373        $crate::entity_id! { $($name),+ ; }
374    };
375    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
376        $(
377            #[derive(
378                $crate::prelude::sqlx::Type,
379                Debug,
380                Clone,
381                Copy,
382                PartialEq,
383                Eq,
384                PartialOrd,
385                Ord,
386                Hash,
387                $crate::prelude::serde::Deserialize,
388                $crate::prelude::serde::Serialize,
389            )]
390            #[serde(crate = "es_entity::prelude::serde")]
391            #[serde(transparent)]
392            #[sqlx(transparent)]
393            pub struct $name($crate::prelude::uuid::Uuid);
394            $crate::__entity_id_common_impls!($name);
395            $crate::__entity_id_graphql_impls!($name);
396
397        )+
398        $crate::__entity_id_conversions!($($from => $to),*);
399    };
400}
401
402#[doc(hidden)]
403#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
404#[macro_export]
405macro_rules! entity_id {
406    // Match identifiers without conversions
407    ($($name:ident),+ $(,)?) => {
408        $crate::entity_id! { $($name),+ ; }
409    };
410    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
411        $(
412            #[derive(
413                $crate::prelude::sqlx::Type,
414                Debug,
415                Clone,
416                Copy,
417                PartialEq,
418                Eq,
419                PartialOrd,
420                Ord,
421                Hash,
422                $crate::prelude::serde::Deserialize,
423                $crate::prelude::serde::Serialize,
424                $crate::prelude::schemars::JsonSchema,
425            )]
426            #[schemars(crate = "es_entity::prelude::schemars")]
427            #[serde(crate = "es_entity::prelude::serde")]
428            #[serde(transparent)]
429            #[sqlx(transparent)]
430            pub struct $name($crate::prelude::uuid::Uuid);
431            $crate::__entity_id_common_impls!($name);
432
433        )+
434        $crate::__entity_id_conversions!($($from => $to),*);
435    };
436}
437
438/// Create UUID-wrappers for database operations.
439///
440/// This macro generates type-safe UUID-wrapper structs with trait support for
441/// serialization, database operations, GraphQL integration, and JSON schema generation.
442///
443/// # Features
444///
445/// The macro automatically includes different trait implementations based on enabled features:
446/// - `graphql`: Adds GraphQL UUID conversion traits and registers each entity ID as its own
447///   GraphQL scalar type (e.g. `CustomerId` instead of the generic `UUID`), providing type
448///   safety at the API layer
449/// - `json-schema`: Adds JSON schema generation support
450///
451/// # Generated Traits
452///
453/// All entity IDs automatically implement:
454/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
455/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
456/// - `sqlx::Type` (with transparent database type)
457/// - `Display` and `FromStr` for string conversion
458/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
459///
460/// # Parameters
461///
462/// - `$name`: One or more entity ID type names to create
463/// - `$from => $to`: Optional conversion pairs between different entity ID types
464///
465/// # Examples
466///
467/// ```rust
468/// use es_entity::entity_id;
469///
470/// entity_id! { UserId, OrderId }
471///
472/// // Creates:
473/// // pub struct UserId(Uuid);
474/// // pub struct OrderId(Uuid);
475/// ```
476///
477/// ```rust
478/// use es_entity::entity_id;
479///
480/// entity_id! {
481///     UserId,
482///     AdminUserId;
483///     UserId => AdminUserId
484/// }
485///
486/// // Creates UserId and AdminUserId with `impl From` conversion between them
487/// ```
488#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
489#[macro_export]
490macro_rules! entity_id {
491    // Match identifiers without conversions
492    ($($name:ident),+ $(,)?) => {
493        $crate::entity_id! { $($name),+ ; }
494    };
495    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
496        $(
497            #[derive(
498                $crate::prelude::sqlx::Type,
499                Debug,
500                Clone,
501                Copy,
502                PartialEq,
503                Eq,
504                PartialOrd,
505                Ord,
506                Hash,
507                $crate::prelude::serde::Deserialize,
508                $crate::prelude::serde::Serialize,
509            )]
510            #[serde(crate = "es_entity::prelude::serde")]
511            #[serde(transparent)]
512            #[sqlx(transparent)]
513            pub struct $name($crate::prelude::uuid::Uuid);
514            $crate::__entity_id_common_impls!($name);
515
516        )+
517        $crate::__entity_id_conversions!($($from => $to),*);
518    };
519}