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 + forgettable
176    (
177        entity = $entity:ident,
178        forgettable_tbl = $forgettable_tbl:literal,
179        $query:expr,
180        $($args:tt)*
181    ) => ({
182        $crate::expand_es_query!(
183            entity = $entity,
184            forgettable_tbl = $forgettable_tbl,
185            sql = $query,
186            args = [$($args)*]
187        )
188    });
189    // With entity override + forgettable - no args
190    (
191        entity = $entity:ident,
192        forgettable_tbl = $forgettable_tbl:literal,
193        $query:expr
194    ) => ({
195        $crate::expand_es_query!(
196            entity = $entity,
197            forgettable_tbl = $forgettable_tbl,
198            sql = $query
199        )
200    });
201
202    // With entity override
203    (
204        entity = $entity:ident,
205        $query:expr,
206        $($args:tt)*
207    ) => ({
208        $crate::expand_es_query!(
209            entity = $entity,
210            sql = $query,
211            args = [$($args)*]
212        )
213    });
214    // With entity override - no args
215    (
216        entity = $entity:ident,
217        $query:expr
218    ) => ({
219        $crate::expand_es_query!(
220            entity = $entity,
221            sql = $query
222        )
223    });
224
225    // With tbl_prefix + forgettable
226    (
227        tbl_prefix = $tbl_prefix:literal,
228        forgettable_tbl = $forgettable_tbl:literal,
229        $query:expr,
230        $($args:tt)*
231    ) => ({
232        $crate::expand_es_query!(
233            tbl_prefix = $tbl_prefix,
234            forgettable_tbl = $forgettable_tbl,
235            sql = $query,
236            args = [$($args)*]
237        )
238    });
239    // With tbl_prefix + forgettable - no args
240    (
241        tbl_prefix = $tbl_prefix:literal,
242        forgettable_tbl = $forgettable_tbl:literal,
243        $query:expr
244    ) => ({
245        $crate::expand_es_query!(
246            tbl_prefix = $tbl_prefix,
247            forgettable_tbl = $forgettable_tbl,
248            sql = $query
249        )
250    });
251
252    // With tbl_prefix
253    (
254        tbl_prefix = $tbl_prefix:literal,
255        $query:expr,
256        $($args:tt)*
257    ) => ({
258        $crate::expand_es_query!(
259            tbl_prefix = $tbl_prefix,
260            sql = $query,
261            args = [$($args)*]
262        )
263    });
264    // With tbl_prefix - no args
265    (
266        tbl_prefix = $tbl_prefix:literal,
267        $query:expr
268    ) => ({
269        $crate::expand_es_query!(
270            tbl_prefix = $tbl_prefix,
271            sql = $query
272        )
273    });
274
275    // Basic form
276    (
277        $query:expr,
278        $($args:tt)*
279    ) => ({
280        $crate::expand_es_query!(
281            sql = $query,
282            args = [$($args)*]
283        )
284    });
285    // Basic form - no args
286    (
287        $query:expr
288    ) => ({
289        $crate::expand_es_query!(
290            sql = $query
291        )
292    });
293}
294
295// Helper macro for common entity_id implementations (internal use only)
296#[doc(hidden)]
297#[macro_export]
298macro_rules! __entity_id_common_impls {
299    ($name:ident) => {
300        impl $name {
301            #[allow(clippy::new_without_default)]
302            pub fn new() -> Self {
303                $crate::prelude::uuid::Uuid::now_v7().into()
304            }
305        }
306
307        impl From<$crate::prelude::uuid::Uuid> for $name {
308            fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
309                Self(uuid)
310            }
311        }
312
313        impl From<$name> for $crate::prelude::uuid::Uuid {
314            fn from(id: $name) -> Self {
315                id.0
316            }
317        }
318
319        impl From<&$name> for $crate::prelude::uuid::Uuid {
320            fn from(id: &$name) -> Self {
321                id.0
322            }
323        }
324
325        impl std::fmt::Display for $name {
326            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327                write!(f, "{}", self.0)
328            }
329        }
330
331        impl std::str::FromStr for $name {
332            type Err = $crate::prelude::uuid::Error;
333
334            fn from_str(s: &str) -> Result<Self, Self::Err> {
335                Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
336            }
337        }
338    };
339}
340
341// Helper macro for GraphQL-specific entity_id implementations (internal use only)
342// When `graphql` feature is enabled, entity IDs become their own GraphQL scalars
343// (e.g. `CustomerId` instead of the generic `UUID`), providing type safety at the API layer.
344#[doc(hidden)]
345#[macro_export]
346macro_rules! __entity_id_graphql_impls {
347    ($name:ident) => {
348        impl From<$crate::graphql::UUID> for $name {
349            fn from(id: $crate::graphql::UUID) -> Self {
350                $name($crate::prelude::uuid::Uuid::from(&id))
351            }
352        }
353
354        impl From<&$crate::graphql::UUID> for $name {
355            fn from(id: &$crate::graphql::UUID) -> Self {
356                $name($crate::prelude::uuid::Uuid::from(id))
357            }
358        }
359
360        $crate::graphql::async_graphql::scalar!($name);
361    };
362}
363
364// Helper macro for additional conversions (internal use only)
365#[doc(hidden)]
366#[macro_export]
367macro_rules! __entity_id_conversions {
368    ($($from:ty => $to:ty),* $(,)?) => {
369        $(
370            impl From<$from> for $to {
371                fn from(id: $from) -> Self {
372                    <$to>::from($crate::prelude::uuid::Uuid::from(id))
373                }
374            }
375            impl From<$to> for $from {
376                fn from(id: $to) -> Self {
377                    <$from>::from($crate::prelude::uuid::Uuid::from(id))
378                }
379            }
380        )*
381    };
382}
383
384#[doc(hidden)]
385#[cfg(all(feature = "graphql", feature = "json-schema"))]
386#[macro_export]
387macro_rules! entity_id {
388    // Match identifiers without conversions
389    ($($name:ident),+ $(,)?) => {
390        $crate::entity_id! { $($name),+ ; }
391    };
392    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
393        $(
394            #[derive(
395                $crate::prelude::sqlx::Type,
396                Debug,
397                Clone,
398                Copy,
399                PartialEq,
400                Eq,
401                PartialOrd,
402                Ord,
403                Hash,
404                $crate::prelude::serde::Deserialize,
405                $crate::prelude::serde::Serialize,
406                $crate::prelude::schemars::JsonSchema,
407            )]
408            #[schemars(crate = "es_entity::prelude::schemars")]
409            #[serde(crate = "es_entity::prelude::serde")]
410            #[serde(transparent)]
411            #[sqlx(transparent)]
412            pub struct $name($crate::prelude::uuid::Uuid);
413            $crate::__entity_id_common_impls!($name);
414            $crate::__entity_id_graphql_impls!($name);
415
416        )+
417        $crate::__entity_id_conversions!($($from => $to),*);
418    };
419}
420
421#[doc(hidden)]
422#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
423#[macro_export]
424macro_rules! entity_id {
425    // Match identifiers without conversions
426    ($($name:ident),+ $(,)?) => {
427        $crate::entity_id! { $($name),+ ; }
428    };
429    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
430        $(
431            #[derive(
432                $crate::prelude::sqlx::Type,
433                Debug,
434                Clone,
435                Copy,
436                PartialEq,
437                Eq,
438                PartialOrd,
439                Ord,
440                Hash,
441                $crate::prelude::serde::Deserialize,
442                $crate::prelude::serde::Serialize,
443            )]
444            #[serde(crate = "es_entity::prelude::serde")]
445            #[serde(transparent)]
446            #[sqlx(transparent)]
447            pub struct $name($crate::prelude::uuid::Uuid);
448            $crate::__entity_id_common_impls!($name);
449            $crate::__entity_id_graphql_impls!($name);
450
451        )+
452        $crate::__entity_id_conversions!($($from => $to),*);
453    };
454}
455
456#[doc(hidden)]
457#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
458#[macro_export]
459macro_rules! entity_id {
460    // Match identifiers without conversions
461    ($($name:ident),+ $(,)?) => {
462        $crate::entity_id! { $($name),+ ; }
463    };
464    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
465        $(
466            #[derive(
467                $crate::prelude::sqlx::Type,
468                Debug,
469                Clone,
470                Copy,
471                PartialEq,
472                Eq,
473                PartialOrd,
474                Ord,
475                Hash,
476                $crate::prelude::serde::Deserialize,
477                $crate::prelude::serde::Serialize,
478                $crate::prelude::schemars::JsonSchema,
479            )]
480            #[schemars(crate = "es_entity::prelude::schemars")]
481            #[serde(crate = "es_entity::prelude::serde")]
482            #[serde(transparent)]
483            #[sqlx(transparent)]
484            pub struct $name($crate::prelude::uuid::Uuid);
485            $crate::__entity_id_common_impls!($name);
486
487        )+
488        $crate::__entity_id_conversions!($($from => $to),*);
489    };
490}
491
492/// Create UUID-wrappers for database operations.
493///
494/// This macro generates type-safe UUID-wrapper structs with trait support for
495/// serialization, database operations, GraphQL integration, and JSON schema generation.
496///
497/// # Features
498///
499/// The macro automatically includes different trait implementations based on enabled features:
500/// - `graphql`: Adds GraphQL UUID conversion traits and registers each entity ID as its own
501///   GraphQL scalar type (e.g. `CustomerId` instead of the generic `UUID`), providing type
502///   safety at the API layer
503/// - `json-schema`: Adds JSON schema generation support
504///
505/// # Generated Traits
506///
507/// All entity IDs automatically implement:
508/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
509/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
510/// - `sqlx::Type` (with transparent database type)
511/// - `Display` and `FromStr` for string conversion
512/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
513///
514/// # Parameters
515///
516/// - `$name`: One or more entity ID type names to create
517/// - `$from => $to`: Optional conversion pairs between different entity ID types
518///
519/// # Examples
520///
521/// ```rust
522/// use es_entity::entity_id;
523///
524/// entity_id! { UserId, OrderId }
525///
526/// // Creates:
527/// // pub struct UserId(Uuid);
528/// // pub struct OrderId(Uuid);
529/// ```
530///
531/// ```rust
532/// use es_entity::entity_id;
533///
534/// entity_id! {
535///     UserId,
536///     AdminUserId;
537///     UserId => AdminUserId
538/// }
539///
540/// // Creates UserId and AdminUserId with `impl From` conversion between them
541/// ```
542#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
543#[macro_export]
544macro_rules! entity_id {
545    // Match identifiers without conversions
546    ($($name:ident),+ $(,)?) => {
547        $crate::entity_id! { $($name),+ ; }
548    };
549    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
550        $(
551            #[derive(
552                $crate::prelude::sqlx::Type,
553                Debug,
554                Clone,
555                Copy,
556                PartialEq,
557                Eq,
558                PartialOrd,
559                Ord,
560                Hash,
561                $crate::prelude::serde::Deserialize,
562                $crate::prelude::serde::Serialize,
563            )]
564            #[serde(crate = "es_entity::prelude::serde")]
565            #[serde(transparent)]
566            #[sqlx(transparent)]
567            pub struct $name($crate::prelude::uuid::Uuid);
568            $crate::__entity_id_common_impls!($name);
569
570        )+
571        $crate::__entity_id_conversions!($($from => $to),*);
572    };
573}