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