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}