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///
102/// ## Snapshotted streams: the `snapshot:` clause
103///
104/// Iterating `.replay()` (or `.replay_persisted()`) on an `EntityEvents<E, S>`
105/// with a real snapshot type yields [`Replay`][crate::Replay] items instead
106/// of plain events. `already_applied:` and `resets_on:` patterns still match
107/// `&E` values (wrapped in `Replay::Event` under the hood via
108/// [`IntoReplay`][crate::IntoReplay]), so existing call sites over
109/// `iter_all().rev()` compile unchanged. A stream that can yield
110/// `Replay::Snapshot` additionally needs a `snapshot:` clause — placed last,
111/// after `resets_on:` if present — saying what the snapshot implies about
112/// this operation. Omitting it on such a stream is a compile error: the
113/// macro expands to a call the snapshot's type does not implement, with a
114/// diagnostic pointing at the missing clause. Use `snapshot: _ if false` to
115/// say explicitly "the snapshot can never imply this was already applied".
116///
117/// `resets_on` semantics fold into the guard the same way: if the reset
118/// condition is itself something the snapshot could already reflect, encode
119/// the resolved state in the `snapshot:` guard (e.g.
120/// `snapshot: s if s.last_threshold_update == Some((lower, upper))`) rather
121/// than in a separate clause — the snapshot has no "reset" of its own, it is
122/// always the fold as of its sequence.
123///
124/// ```rust
125/// use es_entity::*;
126/// use serde::{Serialize, Deserialize};
127///
128/// entity_id! { MeterId }
129///
130/// #[derive(EsSnapshot, Serialize, Deserialize, Debug)]
131/// #[es_snapshot(version = 1)]
132/// pub struct MeterSnapshot { id: MeterId, last_value: Option<i64> }
133///
134/// #[derive(EsEvent, Serialize, Deserialize)]
135/// #[serde(tag = "type", rename_all = "snake_case")]
136/// #[es_event(id = "MeterId")]
137/// pub enum MeterEvent {
138/// Initialized { id: MeterId },
139/// ReadingRecorded { value: i64 },
140/// }
141///
142/// pub struct NewMeter { id: MeterId }
143/// impl IntoEvents<MeterEvent> for NewMeter {
144/// fn into_events(self) -> EntityEvents<MeterEvent> {
145/// EntityEvents::init(self.id, [MeterEvent::Initialized { id: self.id }])
146/// }
147/// }
148///
149/// #[derive(EsEntity)]
150/// pub struct Meter {
151/// pub id: MeterId,
152/// events: EntityEvents<MeterEvent, MeterSnapshot>,
153/// }
154///
155/// impl TryFromEvents<MeterEvent, MeterSnapshot> for Meter {
156/// fn try_from_events(
157/// events: EntityEvents<MeterEvent, MeterSnapshot>,
158/// ) -> Result<Self, EntityHydrationError> {
159/// let mut id = None;
160/// for r in events.replay() {
161/// match r {
162/// Replay::Snapshot(s) => id = Some(s.id),
163/// Replay::Event(MeterEvent::Initialized { id: i }) => id = Some(*i),
164/// Replay::Event(_) => {}
165/// }
166/// }
167/// Ok(Meter { id: id.expect("Initialized"), events })
168/// }
169/// }
170///
171/// impl Meter {
172/// pub fn record(&mut self, value: i64) -> Idempotent<()> {
173/// idempotency_guard!(
174/// self.events.replay().rev(),
175/// already_applied: MeterEvent::ReadingRecorded { value: v } if *v == value,
176/// snapshot: s if s.last_value == Some(value),
177/// );
178/// self.events.push(MeterEvent::ReadingRecorded { value });
179/// Idempotent::Executed(())
180/// }
181/// }
182///
183/// let id = MeterId::new();
184/// let events = EntityEvents::init(id, [MeterEvent::Initialized { id }]).widen_snapshot();
185/// let mut meter = <Meter as TryFromEvents<_, _>>::try_from_events(events).unwrap();
186/// assert!(meter.record(10).did_execute());
187/// assert!(meter.record(10).was_already_applied());
188/// ```
189///
190/// Omitting the `snapshot:` clause on the same stream does not compile:
191///
192/// ```compile_fail
193/// # use es_entity::*;
194/// # use serde::{Serialize, Deserialize};
195/// # entity_id! { MeterId }
196/// # #[derive(EsSnapshot, Serialize, Deserialize, Debug)]
197/// # #[es_snapshot(version = 1)]
198/// # pub struct MeterSnapshot { id: MeterId }
199/// # #[derive(EsEvent, Serialize, Deserialize)]
200/// # #[serde(tag = "type", rename_all = "snake_case")]
201/// # #[es_event(id = "MeterId")]
202/// # pub enum MeterEvent { Initialized { id: MeterId }, ReadingRecorded { value: i64 } }
203/// # pub struct NewMeter { id: MeterId }
204/// # impl IntoEvents<MeterEvent> for NewMeter {
205/// # fn into_events(self) -> EntityEvents<MeterEvent> {
206/// # EntityEvents::init(self.id, [MeterEvent::Initialized { id: self.id }])
207/// # }
208/// # }
209/// # #[derive(EsEntity)]
210/// # pub struct Meter { pub id: MeterId, events: EntityEvents<MeterEvent, MeterSnapshot> }
211/// # impl TryFromEvents<MeterEvent, MeterSnapshot> for Meter {
212/// # fn try_from_events(events: EntityEvents<MeterEvent, MeterSnapshot>) -> Result<Self, EntityHydrationError> {
213/// # unimplemented!()
214/// # }
215/// # }
216/// impl Meter {
217/// pub fn record(&mut self, value: i64) -> Idempotent<()> {
218/// // error: missing the required `snapshot:` clause.
219/// idempotency_guard!(
220/// self.events.replay().rev(),
221/// already_applied: MeterEvent::ReadingRecorded { value: v } if *v == value,
222/// );
223/// self.events.push(MeterEvent::ReadingRecorded { value });
224/// Idempotent::Executed(())
225/// }
226/// }
227/// ```
228#[macro_export]
229macro_rules! idempotency_guard {
230 // already_applied+ , resets_on , snapshot
231 ($events:expr,
232 $(already_applied: $pattern:pat $(if $guard:expr)? ,)+
233 resets_on: $break_pattern:pat $(if $break_guard:expr)? ,
234 snapshot: $snap_pattern:pat $(if $snap_guard:expr)? $(,)?) => {
235 for __item in $events {
236 match $crate::IntoReplay::into_replay(__item) {
237 $(
238 $crate::Replay::Event($pattern) $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
239 )+
240 $crate::Replay::Snapshot($snap_pattern) $(if $snap_guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
241 $crate::Replay::Event($break_pattern) $(if $break_guard)? => break,
242 _ => {}
243 }
244 }
245 };
246 // already_applied+ , resets_on (no snapshot clause -> compile error on a snapshotted stream)
247 ($events:expr,
248 $(already_applied: $pattern:pat $(if $guard:expr)? ,)+
249 resets_on: $break_pattern:pat $(if $break_guard:expr)? $(,)?) => {
250 for __item in $events {
251 match $crate::IntoReplay::into_replay(__item) {
252 $(
253 $crate::Replay::Event($pattern) $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
254 )+
255 $crate::Replay::Snapshot(__s) => return $crate::GuardWithoutSnapshotClause::reached(__s),
256 $crate::Replay::Event($break_pattern) $(if $break_guard)? => break,
257 _ => {}
258 }
259 }
260 };
261 // already_applied+ , snapshot
262 ($events:expr,
263 $(already_applied: $pattern:pat $(if $guard:expr)? ,)+
264 snapshot: $snap_pattern:pat $(if $snap_guard:expr)? $(,)?) => {
265 for __item in $events {
266 match $crate::IntoReplay::into_replay(__item) {
267 $(
268 $crate::Replay::Event($pattern) $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
269 )+
270 $crate::Replay::Snapshot($snap_pattern) $(if $snap_guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
271 _ => {}
272 }
273 }
274 };
275 // already_applied+ only (no snapshot clause -> compile error on a snapshotted stream)
276 ($events:expr,
277 $(already_applied: $pattern:pat $(if $guard:expr)?),+ $(,)?) => {
278 for __item in $events {
279 match $crate::IntoReplay::into_replay(__item) {
280 $(
281 $crate::Replay::Event($pattern) $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
282 )+
283 $crate::Replay::Snapshot(__s) => return $crate::GuardWithoutSnapshotClause::reached(__s),
284 _ => {}
285 }
286 }
287 };
288}
289
290/// Execute an event-sourced query with automatic entity hydration.
291///
292/// Executes user-defined queries and returns entities by internally
293/// joining with events table to hydrate entities, essentially giving the
294/// illusion of working with just the index table.
295///
296/// **Important**: This macro only works inside functions (`fn`) that are defined
297/// within structs that have `#[derive(EsRepo)]` applied. The macro relies on
298/// the repository context to properly hydrate entities.
299///
300/// # Returns
301///
302/// Returns an [`EsQuery`](crate::query::EsQuery) struct that provides methods
303/// like [`fetch_optional()`](crate::query::EsQuery::fetch_optional) and
304/// [`fetch_n()`](crate::query::EsQuery::fetch_n) for executing the
305/// query and retrieving hydrated entities.
306///
307/// # Parameters
308///
309/// - `tbl_prefix`: Table prefix to ignore when deriving entity names from table names (optional)
310/// - `entity`: Override the entity type (optional, useful when table name doesn't match entity name)
311/// - SQL query string
312/// - Additional arguments for the SQL query (optional)
313///
314/// # Examples
315/// ```ignore
316/// // Basic usage
317/// es_query!("SELECT id FROM users WHERE id = $1", id)
318///
319/// // With table prefix
320/// es_query!(
321/// tbl_prefix = "app",
322/// "SELECT id FROM app_users WHERE active = true"
323/// )
324///
325/// // With custom entity type
326/// es_query!(
327/// entity = User,
328/// "SELECT id FROM custom_users_table WHERE id = $1",
329/// id as UserId
330/// )
331/// ```
332#[macro_export]
333macro_rules! es_query {
334 // With entity override + forgettable + snapshot
335 (
336 entity = $entity:ident,
337 forgettable_tbl = $forgettable_tbl:literal,
338 snapshot_tbl = $snapshot_tbl:literal,
339 $query:expr,
340 $($args:tt)*
341 ) => ({
342 $crate::expand_es_query!(
343 entity = $entity,
344 forgettable_tbl = $forgettable_tbl,
345 snapshot_tbl = $snapshot_tbl,
346 sql = $query,
347 args = [$($args)*]
348 )
349 });
350 // With entity override + forgettable + snapshot - no args
351 (
352 entity = $entity:ident,
353 forgettable_tbl = $forgettable_tbl:literal,
354 snapshot_tbl = $snapshot_tbl:literal,
355 $query:expr
356 ) => ({
357 $crate::expand_es_query!(
358 entity = $entity,
359 forgettable_tbl = $forgettable_tbl,
360 snapshot_tbl = $snapshot_tbl,
361 sql = $query
362 )
363 });
364 // With entity override + snapshot (no forgettable)
365 (
366 entity = $entity:ident,
367 snapshot_tbl = $snapshot_tbl:literal,
368 $query:expr,
369 $($args:tt)*
370 ) => ({
371 $crate::expand_es_query!(
372 entity = $entity,
373 snapshot_tbl = $snapshot_tbl,
374 sql = $query,
375 args = [$($args)*]
376 )
377 });
378 // With entity override + snapshot (no forgettable) - no args
379 (
380 entity = $entity:ident,
381 snapshot_tbl = $snapshot_tbl:literal,
382 $query:expr
383 ) => ({
384 $crate::expand_es_query!(
385 entity = $entity,
386 snapshot_tbl = $snapshot_tbl,
387 sql = $query
388 )
389 });
390
391 // With entity override + forgettable
392 (
393 entity = $entity:ident,
394 forgettable_tbl = $forgettable_tbl:literal,
395 $query:expr,
396 $($args:tt)*
397 ) => ({
398 $crate::expand_es_query!(
399 entity = $entity,
400 forgettable_tbl = $forgettable_tbl,
401 sql = $query,
402 args = [$($args)*]
403 )
404 });
405 // With entity override + forgettable - no args
406 (
407 entity = $entity:ident,
408 forgettable_tbl = $forgettable_tbl:literal,
409 $query:expr
410 ) => ({
411 $crate::expand_es_query!(
412 entity = $entity,
413 forgettable_tbl = $forgettable_tbl,
414 sql = $query
415 )
416 });
417
418 // With entity override
419 (
420 entity = $entity:ident,
421 $query:expr,
422 $($args:tt)*
423 ) => ({
424 $crate::expand_es_query!(
425 entity = $entity,
426 sql = $query,
427 args = [$($args)*]
428 )
429 });
430 // With entity override - no args
431 (
432 entity = $entity:ident,
433 $query:expr
434 ) => ({
435 $crate::expand_es_query!(
436 entity = $entity,
437 sql = $query
438 )
439 });
440
441 // With tbl_prefix + forgettable + snapshot
442 (
443 tbl_prefix = $tbl_prefix:literal,
444 forgettable_tbl = $forgettable_tbl:literal,
445 snapshot_tbl = $snapshot_tbl:literal,
446 $query:expr,
447 $($args:tt)*
448 ) => ({
449 $crate::expand_es_query!(
450 tbl_prefix = $tbl_prefix,
451 forgettable_tbl = $forgettable_tbl,
452 snapshot_tbl = $snapshot_tbl,
453 sql = $query,
454 args = [$($args)*]
455 )
456 });
457 // With tbl_prefix + forgettable + snapshot - no args
458 (
459 tbl_prefix = $tbl_prefix:literal,
460 forgettable_tbl = $forgettable_tbl:literal,
461 snapshot_tbl = $snapshot_tbl:literal,
462 $query:expr
463 ) => ({
464 $crate::expand_es_query!(
465 tbl_prefix = $tbl_prefix,
466 forgettable_tbl = $forgettable_tbl,
467 snapshot_tbl = $snapshot_tbl,
468 sql = $query
469 )
470 });
471 // With tbl_prefix + snapshot (no forgettable)
472 (
473 tbl_prefix = $tbl_prefix:literal,
474 snapshot_tbl = $snapshot_tbl:literal,
475 $query:expr,
476 $($args:tt)*
477 ) => ({
478 $crate::expand_es_query!(
479 tbl_prefix = $tbl_prefix,
480 snapshot_tbl = $snapshot_tbl,
481 sql = $query,
482 args = [$($args)*]
483 )
484 });
485 // With tbl_prefix + snapshot (no forgettable) - no args
486 (
487 tbl_prefix = $tbl_prefix:literal,
488 snapshot_tbl = $snapshot_tbl:literal,
489 $query:expr
490 ) => ({
491 $crate::expand_es_query!(
492 tbl_prefix = $tbl_prefix,
493 snapshot_tbl = $snapshot_tbl,
494 sql = $query
495 )
496 });
497
498 // With tbl_prefix + forgettable
499 (
500 tbl_prefix = $tbl_prefix:literal,
501 forgettable_tbl = $forgettable_tbl:literal,
502 $query:expr,
503 $($args:tt)*
504 ) => ({
505 $crate::expand_es_query!(
506 tbl_prefix = $tbl_prefix,
507 forgettable_tbl = $forgettable_tbl,
508 sql = $query,
509 args = [$($args)*]
510 )
511 });
512 // With tbl_prefix + forgettable - no args
513 (
514 tbl_prefix = $tbl_prefix:literal,
515 forgettable_tbl = $forgettable_tbl:literal,
516 $query:expr
517 ) => ({
518 $crate::expand_es_query!(
519 tbl_prefix = $tbl_prefix,
520 forgettable_tbl = $forgettable_tbl,
521 sql = $query
522 )
523 });
524
525 // With tbl_prefix
526 (
527 tbl_prefix = $tbl_prefix:literal,
528 $query:expr,
529 $($args:tt)*
530 ) => ({
531 $crate::expand_es_query!(
532 tbl_prefix = $tbl_prefix,
533 sql = $query,
534 args = [$($args)*]
535 )
536 });
537 // With tbl_prefix - no args
538 (
539 tbl_prefix = $tbl_prefix:literal,
540 $query:expr
541 ) => ({
542 $crate::expand_es_query!(
543 tbl_prefix = $tbl_prefix,
544 sql = $query
545 )
546 });
547
548 // Basic form
549 (
550 $query:expr,
551 $($args:tt)*
552 ) => ({
553 $crate::expand_es_query!(
554 sql = $query,
555 args = [$($args)*]
556 )
557 });
558 // Basic form - no args
559 (
560 $query:expr
561 ) => ({
562 $crate::expand_es_query!(
563 sql = $query
564 )
565 });
566}
567
568// Helper macro for common entity_id implementations (internal use only)
569#[doc(hidden)]
570#[macro_export]
571macro_rules! __entity_id_common_impls {
572 ($name:ident) => {
573 impl $name {
574 #[allow(clippy::new_without_default)]
575 pub fn new() -> Self {
576 $crate::prelude::uuid::Uuid::now_v7().into()
577 }
578 }
579
580 impl From<$crate::prelude::uuid::Uuid> for $name {
581 fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
582 Self(uuid)
583 }
584 }
585
586 impl From<$name> for $crate::prelude::uuid::Uuid {
587 fn from(id: $name) -> Self {
588 id.0
589 }
590 }
591
592 impl From<&$name> for $crate::prelude::uuid::Uuid {
593 fn from(id: &$name) -> Self {
594 id.0
595 }
596 }
597
598 impl std::fmt::Display for $name {
599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600 write!(f, "{}", self.0)
601 }
602 }
603
604 impl std::str::FromStr for $name {
605 type Err = $crate::prelude::uuid::Error;
606
607 fn from_str(s: &str) -> Result<Self, Self::Err> {
608 Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
609 }
610 }
611 };
612}
613
614// Helper macro for GraphQL-specific entity_id implementations (internal use only)
615// When `graphql` feature is enabled, entity IDs become their own GraphQL scalars
616// (e.g. `CustomerId` instead of the generic `UUID`), providing type safety at the API layer.
617#[doc(hidden)]
618#[macro_export]
619macro_rules! __entity_id_graphql_impls {
620 ($name:ident) => {
621 impl From<$crate::graphql::UUID> for $name {
622 fn from(id: $crate::graphql::UUID) -> Self {
623 $name($crate::prelude::uuid::Uuid::from(&id))
624 }
625 }
626
627 impl From<&$crate::graphql::UUID> for $name {
628 fn from(id: &$crate::graphql::UUID) -> Self {
629 $name($crate::prelude::uuid::Uuid::from(id))
630 }
631 }
632
633 $crate::graphql::async_graphql::scalar!($name);
634 };
635}
636
637// Helper macro for additional conversions (internal use only)
638#[doc(hidden)]
639#[macro_export]
640macro_rules! __entity_id_conversions {
641 ($($from:ty => $to:ty),* $(,)?) => {
642 $(
643 impl From<$from> for $to {
644 fn from(id: $from) -> Self {
645 <$to>::from($crate::prelude::uuid::Uuid::from(id))
646 }
647 }
648 impl From<$to> for $from {
649 fn from(id: $to) -> Self {
650 <$from>::from($crate::prelude::uuid::Uuid::from(id))
651 }
652 }
653 )*
654 };
655}
656
657#[doc(hidden)]
658#[cfg(all(feature = "graphql", feature = "json-schema"))]
659#[macro_export]
660macro_rules! entity_id {
661 // Match identifiers without conversions
662 ($($name:ident),+ $(,)?) => {
663 $crate::entity_id! { $($name),+ ; }
664 };
665 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
666 $(
667 #[derive(
668 $crate::prelude::sqlx::Type,
669 Debug,
670 Clone,
671 Copy,
672 PartialEq,
673 Eq,
674 PartialOrd,
675 Ord,
676 Hash,
677 $crate::prelude::serde::Deserialize,
678 $crate::prelude::serde::Serialize,
679 $crate::prelude::schemars::JsonSchema,
680 )]
681 #[schemars(crate = "es_entity::prelude::schemars")]
682 #[serde(crate = "es_entity::prelude::serde")]
683 #[serde(transparent)]
684 #[sqlx(transparent)]
685 pub struct $name($crate::prelude::uuid::Uuid);
686 $crate::__entity_id_common_impls!($name);
687 $crate::__entity_id_graphql_impls!($name);
688
689 )+
690 $crate::__entity_id_conversions!($($from => $to),*);
691 };
692}
693
694#[doc(hidden)]
695#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
696#[macro_export]
697macro_rules! entity_id {
698 // Match identifiers without conversions
699 ($($name:ident),+ $(,)?) => {
700 $crate::entity_id! { $($name),+ ; }
701 };
702 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
703 $(
704 #[derive(
705 $crate::prelude::sqlx::Type,
706 Debug,
707 Clone,
708 Copy,
709 PartialEq,
710 Eq,
711 PartialOrd,
712 Ord,
713 Hash,
714 $crate::prelude::serde::Deserialize,
715 $crate::prelude::serde::Serialize,
716 )]
717 #[serde(crate = "es_entity::prelude::serde")]
718 #[serde(transparent)]
719 #[sqlx(transparent)]
720 pub struct $name($crate::prelude::uuid::Uuid);
721 $crate::__entity_id_common_impls!($name);
722 $crate::__entity_id_graphql_impls!($name);
723
724 )+
725 $crate::__entity_id_conversions!($($from => $to),*);
726 };
727}
728
729#[doc(hidden)]
730#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
731#[macro_export]
732macro_rules! entity_id {
733 // Match identifiers without conversions
734 ($($name:ident),+ $(,)?) => {
735 $crate::entity_id! { $($name),+ ; }
736 };
737 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
738 $(
739 #[derive(
740 $crate::prelude::sqlx::Type,
741 Debug,
742 Clone,
743 Copy,
744 PartialEq,
745 Eq,
746 PartialOrd,
747 Ord,
748 Hash,
749 $crate::prelude::serde::Deserialize,
750 $crate::prelude::serde::Serialize,
751 $crate::prelude::schemars::JsonSchema,
752 )]
753 #[schemars(crate = "es_entity::prelude::schemars")]
754 #[serde(crate = "es_entity::prelude::serde")]
755 #[serde(transparent)]
756 #[sqlx(transparent)]
757 pub struct $name($crate::prelude::uuid::Uuid);
758 $crate::__entity_id_common_impls!($name);
759
760 )+
761 $crate::__entity_id_conversions!($($from => $to),*);
762 };
763}
764
765/// Create UUID-wrappers for database operations.
766///
767/// This macro generates type-safe UUID-wrapper structs with trait support for
768/// serialization, database operations, GraphQL integration, and JSON schema generation.
769///
770/// # Features
771///
772/// The macro automatically includes different trait implementations based on enabled features:
773/// - `graphql`: Adds GraphQL UUID conversion traits and registers each entity ID as its own
774/// GraphQL scalar type (e.g. `CustomerId` instead of the generic `UUID`), providing type
775/// safety at the API layer
776/// - `json-schema`: Adds JSON schema generation support
777///
778/// # Generated Traits
779///
780/// All entity IDs automatically implement:
781/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
782/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
783/// - `sqlx::Type` (with transparent database type)
784/// - `Display` and `FromStr` for string conversion
785/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
786///
787/// # Parameters
788///
789/// - `$name`: One or more entity ID type names to create
790/// - `$from => $to`: Optional conversion pairs between different entity ID types
791///
792/// # Examples
793///
794/// ```rust
795/// use es_entity::entity_id;
796///
797/// entity_id! { UserId, OrderId }
798///
799/// // Creates:
800/// // pub struct UserId(Uuid);
801/// // pub struct OrderId(Uuid);
802/// ```
803///
804/// ```rust
805/// use es_entity::entity_id;
806///
807/// entity_id! {
808/// UserId,
809/// AdminUserId;
810/// UserId => AdminUserId
811/// }
812///
813/// // Creates UserId and AdminUserId with `impl From` conversion between them
814/// ```
815#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
816#[macro_export]
817macro_rules! entity_id {
818 // Match identifiers without conversions
819 ($($name:ident),+ $(,)?) => {
820 $crate::entity_id! { $($name),+ ; }
821 };
822 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
823 $(
824 #[derive(
825 $crate::prelude::sqlx::Type,
826 Debug,
827 Clone,
828 Copy,
829 PartialEq,
830 Eq,
831 PartialOrd,
832 Ord,
833 Hash,
834 $crate::prelude::serde::Deserialize,
835 $crate::prelude::serde::Serialize,
836 )]
837 #[serde(crate = "es_entity::prelude::serde")]
838 #[serde(transparent)]
839 #[sqlx(transparent)]
840 pub struct $name($crate::prelude::uuid::Uuid);
841 $crate::__entity_id_common_impls!($name);
842
843 )+
844 $crate::__entity_id_conversions!($($from => $to),*);
845 };
846}
847
848/// Implements [`AtomicOperation`](crate::AtomicOperation) for a type by
849/// delegating every method to an operation it holds.
850///
851/// A type that wraps or dispatches to another operation — a newtype over
852/// `&mut DbOp` that seals off `commit()`, a restricted view handed to a
853/// callback, an enum choosing between several ops — needs all of
854/// `AtomicOperation` forwarded. Written by hand that is eight near-identical
855/// bodies per type, and a method left out silently inherits a trait default:
856/// `maybe_now` starts reporting `None`, `supports_hooks` `false`, or
857/// [`savepoint_parts`](crate::AtomicOperation::savepoint_parts) reports no hook
858/// buffer while the wrapped op has one. The behaviour changes and nothing fails
859/// to compile.
860///
861/// This macro generates the whole impl, so those cannot drift apart, and it adds
862/// **no** public accessor — the wrapped operation stays as private as it was.
863/// That matters for types that withhold `&mut` access on purpose: exposing it
864/// would let a caller swap the operation out or commit it directly.
865///
866/// # Newtype
867///
868/// ```rust,ignore
869/// struct FlushOp<'a>(&'a mut es_entity::DbOp<'static>);
870///
871/// es_entity::delegate_atomic_operation!(FlushOp<'_>, { s => s.0 });
872/// ```
873///
874/// # Enum
875///
876/// Each arm names a pattern and the operation to delegate to. Arms may hold
877/// different types — `DbOp`, `&mut DbOp`, `&mut SavepointOp` — because the
878/// generated code calls the method inside each arm rather than unifying the
879/// arms into one value.
880///
881/// ```rust,ignore
882/// es_entity::delegate_atomic_operation!(UseCaseOp<'_, '_>, {
883/// Self::Owned(op) => op,
884/// Self::Db(op) => op,
885/// Self::Savepoint(op) => op,
886/// });
887/// ```
888///
889/// # Generic types
890///
891/// Pass the impl generics in brackets first:
892///
893/// ```rust,ignore
894/// es_entity::delegate_atomic_operation!([<'a, T: es_entity::AtomicOperation>] MyOp<'a, T>, {
895/// s => s.inner
896/// });
897/// ```
898///
899/// # When not to use it
900///
901/// Only for pure delegation. A wrapper that changes behaviour — reporting its
902/// own cached time, refusing hooks — must hand-write the impl, since this macro
903/// forwards every method.
904#[macro_export]
905macro_rules! delegate_atomic_operation {
906 // Bracketed generics first: otherwise the bare arm below tries to parse a
907 // leading `[` as the start of a slice type.
908 ([$($generics:tt)*] $ty:ty, { $($pat:pat => $target:expr),+ $(,)? }) => {
909 impl $($generics)* $crate::AtomicOperation for $ty {
910 fn maybe_now(
911 &self,
912 ) -> Option<$crate::prelude::chrono::DateTime<$crate::prelude::chrono::Utc>> {
913 match self { $($pat => $target.maybe_now()),+ }
914 }
915
916 fn clock(&self) -> &$crate::clock::ClockHandle {
917 match self { $($pat => $target.clock()),+ }
918 }
919
920 fn connection(&mut self) -> &mut $crate::db::Connection {
921 match self { $($pat => $target.connection()),+ }
922 }
923
924 fn as_executor(
925 &mut self,
926 ) -> $crate::OneTimeExecutor<'_, &mut $crate::db::Connection> {
927 match self { $($pat => $target.as_executor()),+ }
928 }
929
930 fn add_commit_hook_dyn(
931 &mut self,
932 type_id: std::any::TypeId,
933 hook: Box<dyn $crate::hooks::DynHook>,
934 ) -> Result<(), Box<dyn $crate::hooks::DynHook>> {
935 match self { $($pat => $target.add_commit_hook_dyn(type_id, hook)),+ }
936 }
937
938 fn commit_hook_dyn(
939 &self,
940 type_id: std::any::TypeId,
941 ) -> Option<&dyn $crate::hooks::DynHook> {
942 match self { $($pat => $target.commit_hook_dyn(type_id)),+ }
943 }
944
945 fn supports_hooks(&self) -> bool {
946 match self { $($pat => $target.supports_hooks()),+ }
947 }
948
949 fn savepoint_parts(
950 &mut self,
951 ) -> (&mut $crate::db::Connection, $crate::HookSlot<'_>) {
952 match self { $($pat => $target.savepoint_parts()),+ }
953 }
954 }
955 };
956
957 // Bare form: no impl generics.
958 ($ty:ty, { $($pat:pat => $target:expr),+ $(,)? }) => {
959 $crate::delegate_atomic_operation!([] $ty, { $($pat => $target),+ });
960 };
961}