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 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_already_applied());
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::FromAlreadyApplied::from_already_applied(),
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::FromAlreadyApplied::from_already_applied(),)+
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_optional()`](crate::query::EsQuery::fetch_optional) and
100/// [`fetch_n()`](crate::query::EsQuery::fetch_n) for executing the
101/// query and retrieving hydrated entities.
102///
103/// # Parameters
104///
105/// - `tbl_prefix`: Table prefix to ignore when deriving entity names from table names (optional)
106/// - `entity`: Override the entity type (optional, useful when table name doesn't match entity name)
107/// - SQL query string
108/// - Additional arguments for the SQL query (optional)
109///
110/// # Examples
111/// ```ignore
112/// // Basic usage
113/// es_query!("SELECT id FROM users WHERE id = $1", id)
114///
115/// // With table prefix
116/// es_query!(
117///     tbl_prefix = "app",
118///     "SELECT id FROM app_users WHERE active = true"
119/// )
120///
121/// // With custom entity type
122/// es_query!(
123///     entity = User,
124///     "SELECT id FROM custom_users_table WHERE id = $1",
125///     id as UserId
126/// )
127/// ```
128#[macro_export]
129macro_rules! es_query {
130    // With entity override
131    (
132        entity = $entity:ident,
133        $query:expr,
134        $($args:tt)*
135    ) => ({
136        $crate::expand_es_query!(
137            entity = $entity,
138            sql = $query,
139            args = [$($args)*]
140        )
141    });
142    // With entity override - no args
143    (
144        entity = $entity:ident,
145        $query:expr
146    ) => ({
147        $crate::expand_es_query!(
148            entity = $entity,
149            sql = $query
150        )
151    });
152
153    // With tbl_prefix
154    (
155        tbl_prefix = $tbl_prefix:literal,
156        $query:expr,
157        $($args:tt)*
158    ) => ({
159        $crate::expand_es_query!(
160            tbl_prefix = $tbl_prefix,
161            sql = $query,
162            args = [$($args)*]
163        )
164    });
165    // With tbl_prefix - no args
166    (
167        tbl_prefix = $tbl_prefix:literal,
168        $query:expr
169    ) => ({
170        $crate::expand_es_query!(
171            tbl_prefix = $tbl_prefix,
172            sql = $query
173        )
174    });
175
176    // Basic form
177    (
178        $query:expr,
179        $($args:tt)*
180    ) => ({
181        $crate::expand_es_query!(
182            sql = $query,
183            args = [$($args)*]
184        )
185    });
186    // Basic form - no args
187    (
188        $query:expr
189    ) => ({
190        $crate::expand_es_query!(
191            sql = $query
192        )
193    });
194}
195
196// Helper macro for common entity_id implementations (internal use only)
197#[doc(hidden)]
198#[macro_export]
199macro_rules! __entity_id_common_impls {
200    ($name:ident) => {
201        impl $name {
202            #[allow(clippy::new_without_default)]
203            pub fn new() -> Self {
204                $crate::prelude::uuid::Uuid::now_v7().into()
205            }
206        }
207
208        impl From<$crate::prelude::uuid::Uuid> for $name {
209            fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
210                Self(uuid)
211            }
212        }
213
214        impl From<$name> for $crate::prelude::uuid::Uuid {
215            fn from(id: $name) -> Self {
216                id.0
217            }
218        }
219
220        impl From<&$name> for $crate::prelude::uuid::Uuid {
221            fn from(id: &$name) -> Self {
222                id.0
223            }
224        }
225
226        impl std::fmt::Display for $name {
227            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228                write!(f, "{}", self.0)
229            }
230        }
231
232        impl std::str::FromStr for $name {
233            type Err = $crate::prelude::uuid::Error;
234
235            fn from_str(s: &str) -> Result<Self, Self::Err> {
236                Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
237            }
238        }
239    };
240}
241
242// Helper macro for GraphQL-specific entity_id implementations (internal use only)
243#[doc(hidden)]
244#[macro_export]
245macro_rules! __entity_id_graphql_impls {
246    ($name:ident) => {
247        impl From<$crate::graphql::UUID> for $name {
248            fn from(id: $crate::graphql::UUID) -> Self {
249                $name($crate::prelude::uuid::Uuid::from(&id))
250            }
251        }
252
253        impl From<&$crate::graphql::UUID> for $name {
254            fn from(id: &$crate::graphql::UUID) -> Self {
255                $name($crate::prelude::uuid::Uuid::from(id))
256            }
257        }
258    };
259}
260
261// Helper macro for additional conversions (internal use only)
262#[doc(hidden)]
263#[macro_export]
264macro_rules! __entity_id_conversions {
265    ($($from:ty => $to:ty),* $(,)?) => {
266        $(
267            impl From<$from> for $to {
268                fn from(id: $from) -> Self {
269                    <$to>::from($crate::prelude::uuid::Uuid::from(id))
270                }
271            }
272            impl From<$to> for $from {
273                fn from(id: $to) -> Self {
274                    <$from>::from($crate::prelude::uuid::Uuid::from(id))
275                }
276            }
277        )*
278    };
279}
280
281#[doc(hidden)]
282#[cfg(all(feature = "graphql", feature = "json-schema"))]
283#[macro_export]
284macro_rules! entity_id {
285    // Match identifiers without conversions
286    ($($name:ident),+ $(,)?) => {
287        $crate::entity_id! { $($name),+ ; }
288    };
289    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
290        $(
291            #[derive(
292                $crate::prelude::sqlx::Type,
293                Debug,
294                Clone,
295                Copy,
296                PartialEq,
297                Eq,
298                PartialOrd,
299                Ord,
300                Hash,
301                $crate::prelude::serde::Deserialize,
302                $crate::prelude::serde::Serialize,
303                $crate::prelude::schemars::JsonSchema,
304            )]
305            #[schemars(crate = "es_entity::prelude::schemars")]
306            #[serde(crate = "es_entity::prelude::serde")]
307            #[serde(transparent)]
308            #[sqlx(transparent)]
309            pub struct $name($crate::prelude::uuid::Uuid);
310            $crate::__entity_id_common_impls!($name);
311            $crate::__entity_id_graphql_impls!($name);
312        )+
313        $crate::__entity_id_conversions!($($from => $to),*);
314    };
315}
316
317#[doc(hidden)]
318#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
319#[macro_export]
320macro_rules! entity_id {
321    // Match identifiers without conversions
322    ($($name:ident),+ $(,)?) => {
323        $crate::entity_id! { $($name),+ ; }
324    };
325    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
326        $(
327            #[derive(
328                $crate::prelude::sqlx::Type,
329                Debug,
330                Clone,
331                Copy,
332                PartialEq,
333                Eq,
334                PartialOrd,
335                Ord,
336                Hash,
337                $crate::prelude::serde::Deserialize,
338                $crate::prelude::serde::Serialize,
339            )]
340            #[serde(crate = "es_entity::prelude::serde")]
341            #[serde(transparent)]
342            #[sqlx(transparent)]
343            pub struct $name($crate::prelude::uuid::Uuid);
344            $crate::__entity_id_common_impls!($name);
345            $crate::__entity_id_graphql_impls!($name);
346        )+
347        $crate::__entity_id_conversions!($($from => $to),*);
348    };
349}
350
351#[doc(hidden)]
352#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
353#[macro_export]
354macro_rules! entity_id {
355    // Match identifiers without conversions
356    ($($name:ident),+ $(,)?) => {
357        $crate::entity_id! { $($name),+ ; }
358    };
359    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
360        $(
361            #[derive(
362                $crate::prelude::sqlx::Type,
363                Debug,
364                Clone,
365                Copy,
366                PartialEq,
367                Eq,
368                PartialOrd,
369                Ord,
370                Hash,
371                $crate::prelude::serde::Deserialize,
372                $crate::prelude::serde::Serialize,
373                $crate::prelude::schemars::JsonSchema,
374            )]
375            #[schemars(crate = "es_entity::prelude::schemars")]
376            #[serde(crate = "es_entity::prelude::serde")]
377            #[serde(transparent)]
378            #[sqlx(transparent)]
379            pub struct $name($crate::prelude::uuid::Uuid);
380            $crate::__entity_id_common_impls!($name);
381        )+
382        $crate::__entity_id_conversions!($($from => $to),*);
383    };
384}
385
386/// Create UUID-wrappers for database operations.
387///
388/// This macro generates type-safe UUID-wrapper structs with trait support for
389/// serialization, database operations, GraphQL integration, and JSON schema generation.
390///
391/// # Features
392///
393/// The macro automatically includes different trait implementations based on enabled features:
394/// - `graphql`: Adds GraphQL UUID conversion traits
395/// - `json-schema`: Adds JSON schema generation support
396///
397/// # Generated Traits
398///
399/// All entity IDs automatically implement:
400/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
401/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
402/// - `sqlx::Type` (with transparent database type)
403/// - `Display` and `FromStr` for string conversion
404/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
405///
406/// # Parameters
407///
408/// - `$name`: One or more entity ID type names to create
409/// - `$from => $to`: Optional conversion pairs between different entity ID types
410///
411/// # Examples
412///
413/// ```rust
414/// use es_entity::entity_id;
415///
416/// entity_id! { UserId, OrderId }
417///
418/// // Creates:
419/// // pub struct UserId(Uuid);
420/// // pub struct OrderId(Uuid);
421/// ```
422///
423/// ```rust
424/// use es_entity::entity_id;
425///
426/// entity_id! {
427///     UserId,
428///     AdminUserId;
429///     UserId => AdminUserId
430/// }
431///
432/// // Creates UserId and AdminUserId with `impl From` conversion between them
433/// ```
434#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
435#[macro_export]
436macro_rules! entity_id {
437    // Match identifiers without conversions
438    ($($name:ident),+ $(,)?) => {
439        $crate::entity_id! { $($name),+ ; }
440    };
441    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
442        $(
443            #[derive(
444                $crate::prelude::sqlx::Type,
445                Debug,
446                Clone,
447                Copy,
448                PartialEq,
449                Eq,
450                PartialOrd,
451                Ord,
452                Hash,
453                $crate::prelude::serde::Deserialize,
454                $crate::prelude::serde::Serialize,
455            )]
456            #[serde(crate = "es_entity::prelude::serde")]
457            #[serde(transparent)]
458            #[sqlx(transparent)]
459            pub struct $name($crate::prelude::uuid::Uuid);
460            $crate::__entity_id_common_impls!($name);
461        )+
462        $crate::__entity_id_conversions!($($from => $to),*);
463    };
464}