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 `resets_on` to allow re-applying after an intervening event.
6///
7/// # Parameters
8///
9/// - `$events`: Event collection to search (usually chronologically reversed)
10/// - `already_applied:` One or more event patterns that indicate the operation was already applied.
11/// Multiple patterns are supported — each is checked independently.
12/// - `resets_on:` Optional event pattern that resets the guard, allowing re-execution.
13/// Use Rust's native or-pattern (`P1 | P2`) to match multiple reset events.
14///
15/// When iterating events in reverse, if a `resets_on` event is found before the
16/// `already_applied` event, the guard allows re-execution. This is useful for
17/// toggle-like operations (freeze/unfreeze) or when a state change should
18/// invalidate a previous idempotency check.
19///
20/// # Examples
21///
22/// ```rust
23/// use es_entity::{idempotency_guard, Idempotent};
24/// pub enum UserEvent{
25/// Initialized {id: u64, name: String},
26/// NameUpdated {name: String}
27/// }
28///
29/// pub struct User{
30/// events: Vec<UserEvent>
31/// }
32///
33/// impl User{
34/// pub fn update_name(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
35/// let name = new_name.into();
36/// idempotency_guard!(
37/// self.events.iter().rev(),
38/// already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name
39/// );
40/// self.events.push(UserEvent::NameUpdated{name});
41/// Idempotent::Executed(())
42/// }
43///
44/// pub fn update_name_resettable(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
45/// let name = new_name.into();
46/// idempotency_guard!(
47/// self.events.iter().rev(),
48/// already_applied: UserEvent::NameUpdated { name: existing_name } if existing_name == &name,
49/// resets_on: UserEvent::NameUpdated {..}
50/// // if any other NameUpdated happened more recently, allow re-applying
51/// );
52/// self.events.push(UserEvent::NameUpdated{name});
53/// Idempotent::Executed(())
54/// }
55/// }
56///
57/// let mut user1 = User{ events: vec![] };
58/// let mut user2 = User{ events: vec![] };
59/// assert!(user1.update_name("Alice").did_execute());
60/// // updating "Alice" again ignored because same event with same name exists
61/// assert!(user1.update_name("Alice").was_already_applied());
62///
63/// assert!(user2.update_name_resettable("Alice").did_execute());
64/// assert!(user2.update_name_resettable("Bob").did_execute());
65/// // updating "Alice" again works because Bob's NameUpdated resets the guard
66/// assert!(user2.update_name_resettable("Alice").did_execute());
67/// ```
68///
69/// ## Multiple `already_applied` patterns
70///
71/// ```rust
72/// use es_entity::{idempotency_guard, Idempotent};
73///
74/// pub enum ConfigEvent {
75/// Initialized { id: u64 },
76/// Updated { key: String, value: String },
77/// KeyRotated { key: String },
78/// }
79///
80/// pub struct Config {
81/// events: Vec<ConfigEvent>,
82/// }
83///
84/// impl Config {
85/// pub fn apply_change(&mut self, key: String, value: String) -> Idempotent<()> {
86/// idempotency_guard!(
87/// self.events.iter().rev(),
88/// already_applied: ConfigEvent::Updated { key: k, value: v } if k == &key && v == &value,
89/// already_applied: ConfigEvent::KeyRotated { key: k } if k == &key,
90/// resets_on: ConfigEvent::Initialized { .. }
91/// );
92/// self.events.push(ConfigEvent::Updated { key, value });
93/// Idempotent::Executed(())
94/// }
95/// }
96///
97/// let mut config = Config { events: vec![] };
98/// assert!(config.apply_change("k".into(), "v".into()).did_execute());
99/// assert!(config.apply_change("k".into(), "v".into()).was_already_applied());
100/// ```
101#[macro_export]
102macro_rules! idempotency_guard {
103 // already_applied + resets_on (must come before already_applied-only to avoid ambiguity)
104 ($events:expr,
105 $(already_applied: $pattern:pat $(if $guard:expr)? ,)+
106 resets_on: $break_pattern:pat $(if $break_guard:expr)? $(,)?) => {
107 for event in $events {
108 match event {
109 $(
110 $pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
111 )+
112 $break_pattern $(if $break_guard)? => break,
113 _ => {}
114 }
115 }
116 };
117 // already_applied only
118 ($events:expr,
119 $(already_applied: $pattern:pat $(if $guard:expr)?),+ $(,)?) => {
120 for event in $events {
121 match event {
122 $(
123 $pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
124 )+
125 _ => {}
126 }
127 }
128 };
129}
130
131/// Execute an event-sourced query with automatic entity hydration.
132///
133/// Executes user-defined queries and returns entities by internally
134/// joining with events table to hydrate entities, essentially giving the
135/// illusion of working with just the index table.
136///
137/// **Important**: This macro only works inside functions (`fn`) that are defined
138/// within structs that have `#[derive(EsRepo)]` applied. The macro relies on
139/// the repository context to properly hydrate entities.
140///
141/// # Returns
142///
143/// Returns an [`EsQuery`](crate::query::EsQuery) struct that provides methods
144/// like [`fetch_optional()`](crate::query::EsQuery::fetch_optional) and
145/// [`fetch_n()`](crate::query::EsQuery::fetch_n) for executing the
146/// query and retrieving hydrated entities.
147///
148/// # Parameters
149///
150/// - `tbl_prefix`: Table prefix to ignore when deriving entity names from table names (optional)
151/// - `entity`: Override the entity type (optional, useful when table name doesn't match entity name)
152/// - SQL query string
153/// - Additional arguments for the SQL query (optional)
154///
155/// # Examples
156/// ```ignore
157/// // Basic usage
158/// es_query!("SELECT id FROM users WHERE id = $1", id)
159///
160/// // With table prefix
161/// es_query!(
162/// tbl_prefix = "app",
163/// "SELECT id FROM app_users WHERE active = true"
164/// )
165///
166/// // With custom entity type
167/// es_query!(
168/// entity = User,
169/// "SELECT id FROM custom_users_table WHERE id = $1",
170/// id as UserId
171/// )
172/// ```
173#[macro_export]
174macro_rules! es_query {
175 // With entity override + forgettable
176 (
177 entity = $entity:ident,
178 forgettable_tbl = $forgettable_tbl:literal,
179 $query:expr,
180 $($args:tt)*
181 ) => ({
182 $crate::expand_es_query!(
183 entity = $entity,
184 forgettable_tbl = $forgettable_tbl,
185 sql = $query,
186 args = [$($args)*]
187 )
188 });
189 // With entity override + forgettable - no args
190 (
191 entity = $entity:ident,
192 forgettable_tbl = $forgettable_tbl:literal,
193 $query:expr
194 ) => ({
195 $crate::expand_es_query!(
196 entity = $entity,
197 forgettable_tbl = $forgettable_tbl,
198 sql = $query
199 )
200 });
201
202 // With entity override
203 (
204 entity = $entity:ident,
205 $query:expr,
206 $($args:tt)*
207 ) => ({
208 $crate::expand_es_query!(
209 entity = $entity,
210 sql = $query,
211 args = [$($args)*]
212 )
213 });
214 // With entity override - no args
215 (
216 entity = $entity:ident,
217 $query:expr
218 ) => ({
219 $crate::expand_es_query!(
220 entity = $entity,
221 sql = $query
222 )
223 });
224
225 // With tbl_prefix + forgettable
226 (
227 tbl_prefix = $tbl_prefix:literal,
228 forgettable_tbl = $forgettable_tbl:literal,
229 $query:expr,
230 $($args:tt)*
231 ) => ({
232 $crate::expand_es_query!(
233 tbl_prefix = $tbl_prefix,
234 forgettable_tbl = $forgettable_tbl,
235 sql = $query,
236 args = [$($args)*]
237 )
238 });
239 // With tbl_prefix + forgettable - no args
240 (
241 tbl_prefix = $tbl_prefix:literal,
242 forgettable_tbl = $forgettable_tbl:literal,
243 $query:expr
244 ) => ({
245 $crate::expand_es_query!(
246 tbl_prefix = $tbl_prefix,
247 forgettable_tbl = $forgettable_tbl,
248 sql = $query
249 )
250 });
251
252 // With tbl_prefix
253 (
254 tbl_prefix = $tbl_prefix:literal,
255 $query:expr,
256 $($args:tt)*
257 ) => ({
258 $crate::expand_es_query!(
259 tbl_prefix = $tbl_prefix,
260 sql = $query,
261 args = [$($args)*]
262 )
263 });
264 // With tbl_prefix - no args
265 (
266 tbl_prefix = $tbl_prefix:literal,
267 $query:expr
268 ) => ({
269 $crate::expand_es_query!(
270 tbl_prefix = $tbl_prefix,
271 sql = $query
272 )
273 });
274
275 // Basic form
276 (
277 $query:expr,
278 $($args:tt)*
279 ) => ({
280 $crate::expand_es_query!(
281 sql = $query,
282 args = [$($args)*]
283 )
284 });
285 // Basic form - no args
286 (
287 $query:expr
288 ) => ({
289 $crate::expand_es_query!(
290 sql = $query
291 )
292 });
293}
294
295// Helper macro for common entity_id implementations (internal use only)
296#[doc(hidden)]
297#[macro_export]
298macro_rules! __entity_id_common_impls {
299 ($name:ident) => {
300 impl $name {
301 #[allow(clippy::new_without_default)]
302 pub fn new() -> Self {
303 $crate::prelude::uuid::Uuid::now_v7().into()
304 }
305 }
306
307 impl From<$crate::prelude::uuid::Uuid> for $name {
308 fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
309 Self(uuid)
310 }
311 }
312
313 impl From<$name> for $crate::prelude::uuid::Uuid {
314 fn from(id: $name) -> Self {
315 id.0
316 }
317 }
318
319 impl From<&$name> for $crate::prelude::uuid::Uuid {
320 fn from(id: &$name) -> Self {
321 id.0
322 }
323 }
324
325 impl std::fmt::Display for $name {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 write!(f, "{}", self.0)
328 }
329 }
330
331 impl std::str::FromStr for $name {
332 type Err = $crate::prelude::uuid::Error;
333
334 fn from_str(s: &str) -> Result<Self, Self::Err> {
335 Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
336 }
337 }
338 };
339}
340
341// Helper macro for GraphQL-specific entity_id implementations (internal use only)
342// When `graphql` feature is enabled, entity IDs become their own GraphQL scalars
343// (e.g. `CustomerId` instead of the generic `UUID`), providing type safety at the API layer.
344#[doc(hidden)]
345#[macro_export]
346macro_rules! __entity_id_graphql_impls {
347 ($name:ident) => {
348 impl From<$crate::graphql::UUID> for $name {
349 fn from(id: $crate::graphql::UUID) -> Self {
350 $name($crate::prelude::uuid::Uuid::from(&id))
351 }
352 }
353
354 impl From<&$crate::graphql::UUID> for $name {
355 fn from(id: &$crate::graphql::UUID) -> Self {
356 $name($crate::prelude::uuid::Uuid::from(id))
357 }
358 }
359
360 $crate::graphql::async_graphql::scalar!($name);
361 };
362}
363
364// Helper macro for additional conversions (internal use only)
365#[doc(hidden)]
366#[macro_export]
367macro_rules! __entity_id_conversions {
368 ($($from:ty => $to:ty),* $(,)?) => {
369 $(
370 impl From<$from> for $to {
371 fn from(id: $from) -> Self {
372 <$to>::from($crate::prelude::uuid::Uuid::from(id))
373 }
374 }
375 impl From<$to> for $from {
376 fn from(id: $to) -> Self {
377 <$from>::from($crate::prelude::uuid::Uuid::from(id))
378 }
379 }
380 )*
381 };
382}
383
384#[doc(hidden)]
385#[cfg(all(feature = "graphql", feature = "json-schema"))]
386#[macro_export]
387macro_rules! entity_id {
388 // Match identifiers without conversions
389 ($($name:ident),+ $(,)?) => {
390 $crate::entity_id! { $($name),+ ; }
391 };
392 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
393 $(
394 #[derive(
395 $crate::prelude::sqlx::Type,
396 Debug,
397 Clone,
398 Copy,
399 PartialEq,
400 Eq,
401 PartialOrd,
402 Ord,
403 Hash,
404 $crate::prelude::serde::Deserialize,
405 $crate::prelude::serde::Serialize,
406 $crate::prelude::schemars::JsonSchema,
407 )]
408 #[schemars(crate = "es_entity::prelude::schemars")]
409 #[serde(crate = "es_entity::prelude::serde")]
410 #[serde(transparent)]
411 #[sqlx(transparent)]
412 pub struct $name($crate::prelude::uuid::Uuid);
413 $crate::__entity_id_common_impls!($name);
414 $crate::__entity_id_graphql_impls!($name);
415
416 )+
417 $crate::__entity_id_conversions!($($from => $to),*);
418 };
419}
420
421#[doc(hidden)]
422#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
423#[macro_export]
424macro_rules! entity_id {
425 // Match identifiers without conversions
426 ($($name:ident),+ $(,)?) => {
427 $crate::entity_id! { $($name),+ ; }
428 };
429 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
430 $(
431 #[derive(
432 $crate::prelude::sqlx::Type,
433 Debug,
434 Clone,
435 Copy,
436 PartialEq,
437 Eq,
438 PartialOrd,
439 Ord,
440 Hash,
441 $crate::prelude::serde::Deserialize,
442 $crate::prelude::serde::Serialize,
443 )]
444 #[serde(crate = "es_entity::prelude::serde")]
445 #[serde(transparent)]
446 #[sqlx(transparent)]
447 pub struct $name($crate::prelude::uuid::Uuid);
448 $crate::__entity_id_common_impls!($name);
449 $crate::__entity_id_graphql_impls!($name);
450
451 )+
452 $crate::__entity_id_conversions!($($from => $to),*);
453 };
454}
455
456#[doc(hidden)]
457#[cfg(all(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 $crate::prelude::schemars::JsonSchema,
479 )]
480 #[schemars(crate = "es_entity::prelude::schemars")]
481 #[serde(crate = "es_entity::prelude::serde")]
482 #[serde(transparent)]
483 #[sqlx(transparent)]
484 pub struct $name($crate::prelude::uuid::Uuid);
485 $crate::__entity_id_common_impls!($name);
486
487 )+
488 $crate::__entity_id_conversions!($($from => $to),*);
489 };
490}
491
492/// Create UUID-wrappers for database operations.
493///
494/// This macro generates type-safe UUID-wrapper structs with trait support for
495/// serialization, database operations, GraphQL integration, and JSON schema generation.
496///
497/// # Features
498///
499/// The macro automatically includes different trait implementations based on enabled features:
500/// - `graphql`: Adds GraphQL UUID conversion traits and registers each entity ID as its own
501/// GraphQL scalar type (e.g. `CustomerId` instead of the generic `UUID`), providing type
502/// safety at the API layer
503/// - `json-schema`: Adds JSON schema generation support
504///
505/// # Generated Traits
506///
507/// All entity IDs automatically implement:
508/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
509/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
510/// - `sqlx::Type` (with transparent database type)
511/// - `Display` and `FromStr` for string conversion
512/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
513///
514/// # Parameters
515///
516/// - `$name`: One or more entity ID type names to create
517/// - `$from => $to`: Optional conversion pairs between different entity ID types
518///
519/// # Examples
520///
521/// ```rust
522/// use es_entity::entity_id;
523///
524/// entity_id! { UserId, OrderId }
525///
526/// // Creates:
527/// // pub struct UserId(Uuid);
528/// // pub struct OrderId(Uuid);
529/// ```
530///
531/// ```rust
532/// use es_entity::entity_id;
533///
534/// entity_id! {
535/// UserId,
536/// AdminUserId;
537/// UserId => AdminUserId
538/// }
539///
540/// // Creates UserId and AdminUserId with `impl From` conversion between them
541/// ```
542#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
543#[macro_export]
544macro_rules! entity_id {
545 // Match identifiers without conversions
546 ($($name:ident),+ $(,)?) => {
547 $crate::entity_id! { $($name),+ ; }
548 };
549 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
550 $(
551 #[derive(
552 $crate::prelude::sqlx::Type,
553 Debug,
554 Clone,
555 Copy,
556 PartialEq,
557 Eq,
558 PartialOrd,
559 Ord,
560 Hash,
561 $crate::prelude::serde::Deserialize,
562 $crate::prelude::serde::Serialize,
563 )]
564 #[serde(crate = "es_entity::prelude::serde")]
565 #[serde(transparent)]
566 #[sqlx(transparent)]
567 pub struct $name($crate::prelude::uuid::Uuid);
568 $crate::__entity_id_common_impls!($name);
569
570 )+
571 $crate::__entity_id_conversions!($($from => $to),*);
572 };
573}