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