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#[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}