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_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            #[schemars(crate = "es_entity::prelude::schemars")]
329            #[serde(crate = "es_entity::prelude::serde")]
330            #[serde(transparent)]
331            #[sqlx(transparent)]
332            pub struct $name($crate::prelude::uuid::Uuid);
333            $crate::__entity_id_common_impls!($name);
334            $crate::__entity_id_graphql_impls!($name);
335        )+
336        $crate::__entity_id_conversions!($($from => $to),*);
337    };
338}
339
340#[doc(hidden)]
341#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
342#[macro_export]
343macro_rules! entity_id {
344    // Match identifiers without conversions
345    ($($name:ident),+ $(,)?) => {
346        $crate::entity_id! { $($name),+ ; }
347    };
348    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
349        $(
350            #[derive(
351                $crate::prelude::sqlx::Type,
352                Debug,
353                Clone,
354                Copy,
355                PartialEq,
356                Eq,
357                PartialOrd,
358                Ord,
359                Hash,
360                $crate::prelude::serde::Deserialize,
361                $crate::prelude::serde::Serialize,
362            )]
363            #[serde(crate = "es_entity::prelude::serde")]
364            #[serde(transparent)]
365            #[sqlx(transparent)]
366            pub struct $name($crate::prelude::uuid::Uuid);
367            $crate::__entity_id_common_impls!($name);
368            $crate::__entity_id_graphql_impls!($name);
369        )+
370        $crate::__entity_id_conversions!($($from => $to),*);
371    };
372}
373
374#[doc(hidden)]
375#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
376#[macro_export]
377macro_rules! entity_id {
378    // Match identifiers without conversions
379    ($($name:ident),+ $(,)?) => {
380        $crate::entity_id! { $($name),+ ; }
381    };
382    ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
383        $(
384            #[derive(
385                $crate::prelude::sqlx::Type,
386                Debug,
387                Clone,
388                Copy,
389                PartialEq,
390                Eq,
391                PartialOrd,
392                Ord,
393                Hash,
394                $crate::prelude::serde::Deserialize,
395                $crate::prelude::serde::Serialize,
396                $crate::prelude::schemars::JsonSchema,
397            )]
398            #[schemars(crate = "es_entity::prelude::schemars")]
399            #[serde(crate = "es_entity::prelude::serde")]
400            #[serde(transparent)]
401            #[sqlx(transparent)]
402            pub struct $name($crate::prelude::uuid::Uuid);
403            $crate::__entity_id_common_impls!($name);
404        )+
405        $crate::__entity_id_conversions!($($from => $to),*);
406    };
407}
408
409/// Create UUID-wrappers for database operations.
410///
411/// This macro generates type-safe UUID-wrapper structs with trait support for
412/// serialization, database operations, GraphQL integration, and JSON schema generation.
413///
414/// # Features
415///
416/// The macro automatically includes different trait implementations based on enabled features:
417/// - `graphql`: Adds GraphQL UUID conversion traits
418/// - `json-schema`: Adds JSON schema generation support
419///
420/// # Generated Traits
421///
422/// All entity IDs automatically implement:
423/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
424/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
425/// - `sqlx::Type` (with transparent database type)
426/// - `Display` and `FromStr` for string conversion
427/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
428///
429/// # Parameters
430///
431/// - `$name`: One or more entity ID type names to create
432/// - `$from => $to`: Optional conversion pairs between different entity ID types
433///
434/// # Examples
435///
436/// ```rust
437/// use es_entity::entity_id;
438///
439/// entity_id! { UserId, OrderId }
440///
441/// // Creates:
442/// // pub struct UserId(Uuid);
443/// // pub struct OrderId(Uuid);
444/// ```
445///
446/// ```rust
447/// use es_entity::entity_id;
448///
449/// entity_id! {
450///     UserId,
451///     AdminUserId;
452///     UserId => AdminUserId
453/// }
454///
455/// // Creates UserId and AdminUserId with `impl From` conversion between them
456/// ```
457#[cfg(all(not(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            )]
479            #[serde(crate = "es_entity::prelude::serde")]
480            #[serde(transparent)]
481            #[sqlx(transparent)]
482            pub struct $name($crate::prelude::uuid::Uuid);
483            $crate::__entity_id_common_impls!($name);
484        )+
485        $crate::__entity_id_conversions!($($from => $to),*);
486    };
487}