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