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
176 (
177 entity = $entity:ident,
178 $query:expr,
179 $($args:tt)*
180 ) => ({
181 $crate::expand_es_query!(
182 entity = $entity,
183 sql = $query,
184 args = [$($args)*]
185 )
186 });
187 // With entity override - no args
188 (
189 entity = $entity:ident,
190 $query:expr
191 ) => ({
192 $crate::expand_es_query!(
193 entity = $entity,
194 sql = $query
195 )
196 });
197
198 // With tbl_prefix
199 (
200 tbl_prefix = $tbl_prefix:literal,
201 $query:expr,
202 $($args:tt)*
203 ) => ({
204 $crate::expand_es_query!(
205 tbl_prefix = $tbl_prefix,
206 sql = $query,
207 args = [$($args)*]
208 )
209 });
210 // With tbl_prefix - no args
211 (
212 tbl_prefix = $tbl_prefix:literal,
213 $query:expr
214 ) => ({
215 $crate::expand_es_query!(
216 tbl_prefix = $tbl_prefix,
217 sql = $query
218 )
219 });
220
221 // Basic form
222 (
223 $query:expr,
224 $($args:tt)*
225 ) => ({
226 $crate::expand_es_query!(
227 sql = $query,
228 args = [$($args)*]
229 )
230 });
231 // Basic form - no args
232 (
233 $query:expr
234 ) => ({
235 $crate::expand_es_query!(
236 sql = $query
237 )
238 });
239}
240
241// Helper macro for common entity_id implementations (internal use only)
242#[doc(hidden)]
243#[macro_export]
244macro_rules! __entity_id_common_impls {
245 ($name:ident) => {
246 impl $name {
247 #[allow(clippy::new_without_default)]
248 pub fn new() -> Self {
249 $crate::prelude::uuid::Uuid::now_v7().into()
250 }
251 }
252
253 impl From<$crate::prelude::uuid::Uuid> for $name {
254 fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
255 Self(uuid)
256 }
257 }
258
259 impl From<$name> for $crate::prelude::uuid::Uuid {
260 fn from(id: $name) -> Self {
261 id.0
262 }
263 }
264
265 impl From<&$name> for $crate::prelude::uuid::Uuid {
266 fn from(id: &$name) -> Self {
267 id.0
268 }
269 }
270
271 impl std::fmt::Display for $name {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 write!(f, "{}", self.0)
274 }
275 }
276
277 impl std::str::FromStr for $name {
278 type Err = $crate::prelude::uuid::Error;
279
280 fn from_str(s: &str) -> Result<Self, Self::Err> {
281 Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
282 }
283 }
284 };
285}
286
287// Helper macro for GraphQL-specific entity_id implementations (internal use only)
288// When `graphql` feature is enabled, entity IDs become their own GraphQL scalars
289// (e.g. `CustomerId` instead of the generic `UUID`), providing type safety at the API layer.
290#[doc(hidden)]
291#[macro_export]
292macro_rules! __entity_id_graphql_impls {
293 ($name:ident) => {
294 impl From<$crate::graphql::UUID> for $name {
295 fn from(id: $crate::graphql::UUID) -> Self {
296 $name($crate::prelude::uuid::Uuid::from(&id))
297 }
298 }
299
300 impl From<&$crate::graphql::UUID> for $name {
301 fn from(id: &$crate::graphql::UUID) -> Self {
302 $name($crate::prelude::uuid::Uuid::from(id))
303 }
304 }
305
306 $crate::graphql::async_graphql::scalar!($name);
307 };
308}
309
310// Helper macro for additional conversions (internal use only)
311#[doc(hidden)]
312#[macro_export]
313macro_rules! __entity_id_conversions {
314 ($($from:ty => $to:ty),* $(,)?) => {
315 $(
316 impl From<$from> for $to {
317 fn from(id: $from) -> Self {
318 <$to>::from($crate::prelude::uuid::Uuid::from(id))
319 }
320 }
321 impl From<$to> for $from {
322 fn from(id: $to) -> Self {
323 <$from>::from($crate::prelude::uuid::Uuid::from(id))
324 }
325 }
326 )*
327 };
328}
329
330#[doc(hidden)]
331#[cfg(all(feature = "graphql", feature = "json-schema"))]
332#[macro_export]
333macro_rules! entity_id {
334 // Match identifiers without conversions
335 ($($name:ident),+ $(,)?) => {
336 $crate::entity_id! { $($name),+ ; }
337 };
338 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
339 $(
340 #[derive(
341 $crate::prelude::sqlx::Type,
342 Debug,
343 Clone,
344 Copy,
345 PartialEq,
346 Eq,
347 PartialOrd,
348 Ord,
349 Hash,
350 $crate::prelude::serde::Deserialize,
351 $crate::prelude::serde::Serialize,
352 $crate::prelude::schemars::JsonSchema,
353 )]
354 #[schemars(crate = "es_entity::prelude::schemars")]
355 #[serde(crate = "es_entity::prelude::serde")]
356 #[serde(transparent)]
357 #[sqlx(transparent)]
358 pub struct $name($crate::prelude::uuid::Uuid);
359 $crate::__entity_id_common_impls!($name);
360 $crate::__entity_id_graphql_impls!($name);
361
362 )+
363 $crate::__entity_id_conversions!($($from => $to),*);
364 };
365}
366
367#[doc(hidden)]
368#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
369#[macro_export]
370macro_rules! entity_id {
371 // Match identifiers without conversions
372 ($($name:ident),+ $(,)?) => {
373 $crate::entity_id! { $($name),+ ; }
374 };
375 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
376 $(
377 #[derive(
378 $crate::prelude::sqlx::Type,
379 Debug,
380 Clone,
381 Copy,
382 PartialEq,
383 Eq,
384 PartialOrd,
385 Ord,
386 Hash,
387 $crate::prelude::serde::Deserialize,
388 $crate::prelude::serde::Serialize,
389 )]
390 #[serde(crate = "es_entity::prelude::serde")]
391 #[serde(transparent)]
392 #[sqlx(transparent)]
393 pub struct $name($crate::prelude::uuid::Uuid);
394 $crate::__entity_id_common_impls!($name);
395 $crate::__entity_id_graphql_impls!($name);
396
397 )+
398 $crate::__entity_id_conversions!($($from => $to),*);
399 };
400}
401
402#[doc(hidden)]
403#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
404#[macro_export]
405macro_rules! entity_id {
406 // Match identifiers without conversions
407 ($($name:ident),+ $(,)?) => {
408 $crate::entity_id! { $($name),+ ; }
409 };
410 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
411 $(
412 #[derive(
413 $crate::prelude::sqlx::Type,
414 Debug,
415 Clone,
416 Copy,
417 PartialEq,
418 Eq,
419 PartialOrd,
420 Ord,
421 Hash,
422 $crate::prelude::serde::Deserialize,
423 $crate::prelude::serde::Serialize,
424 $crate::prelude::schemars::JsonSchema,
425 )]
426 #[schemars(crate = "es_entity::prelude::schemars")]
427 #[serde(crate = "es_entity::prelude::serde")]
428 #[serde(transparent)]
429 #[sqlx(transparent)]
430 pub struct $name($crate::prelude::uuid::Uuid);
431 $crate::__entity_id_common_impls!($name);
432
433 )+
434 $crate::__entity_id_conversions!($($from => $to),*);
435 };
436}
437
438/// Create UUID-wrappers for database operations.
439///
440/// This macro generates type-safe UUID-wrapper structs with trait support for
441/// serialization, database operations, GraphQL integration, and JSON schema generation.
442///
443/// # Features
444///
445/// The macro automatically includes different trait implementations based on enabled features:
446/// - `graphql`: Adds GraphQL UUID conversion traits and registers each entity ID as its own
447/// GraphQL scalar type (e.g. `CustomerId` instead of the generic `UUID`), providing type
448/// safety at the API layer
449/// - `json-schema`: Adds JSON schema generation support
450///
451/// # Generated Traits
452///
453/// All entity IDs automatically implement:
454/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
455/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
456/// - `sqlx::Type` (with transparent database type)
457/// - `Display` and `FromStr` for string conversion
458/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
459///
460/// # Parameters
461///
462/// - `$name`: One or more entity ID type names to create
463/// - `$from => $to`: Optional conversion pairs between different entity ID types
464///
465/// # Examples
466///
467/// ```rust
468/// use es_entity::entity_id;
469///
470/// entity_id! { UserId, OrderId }
471///
472/// // Creates:
473/// // pub struct UserId(Uuid);
474/// // pub struct OrderId(Uuid);
475/// ```
476///
477/// ```rust
478/// use es_entity::entity_id;
479///
480/// entity_id! {
481/// UserId,
482/// AdminUserId;
483/// UserId => AdminUserId
484/// }
485///
486/// // Creates UserId and AdminUserId with `impl From` conversion between them
487/// ```
488#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
489#[macro_export]
490macro_rules! entity_id {
491 // Match identifiers without conversions
492 ($($name:ident),+ $(,)?) => {
493 $crate::entity_id! { $($name),+ ; }
494 };
495 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
496 $(
497 #[derive(
498 $crate::prelude::sqlx::Type,
499 Debug,
500 Clone,
501 Copy,
502 PartialEq,
503 Eq,
504 PartialOrd,
505 Ord,
506 Hash,
507 $crate::prelude::serde::Deserialize,
508 $crate::prelude::serde::Serialize,
509 )]
510 #[serde(crate = "es_entity::prelude::serde")]
511 #[serde(transparent)]
512 #[sqlx(transparent)]
513 pub struct $name($crate::prelude::uuid::Uuid);
514 $crate::__entity_id_common_impls!($name);
515
516 )+
517 $crate::__entity_id_conversions!($($from => $to),*);
518 };
519}