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