es_entity/macros.rs
1/// Prevent duplicate event processing by checking for idempotent operations.
2///
3/// Guards against replaying the same mutation in event-sourced systems.
4/// Returns [`AlreadyApplied`][crate::Idempotent::AlreadyApplied] early if matching events are found, allowing the caller
5/// to skip redundant operations. Use break pattern to allow re-applying past operations.
6///
7/// # Parameters
8///
9/// - `$events`: Event collection to search (usually chronologically reversed)
10/// - `$pattern`: Event patterns that indicate operation already applied
11/// - `$break_pattern`: Optional break pattern to stop searching
12///
13/// # Examples
14///
15/// ```rust
16/// use es_entity::{idempotency_guard, Idempotent};
17/// pub enum UserEvent{
18/// Initialized {id: u64, name: String},
19/// NameUpdated {name: String}
20/// }
21///
22/// pub struct User{
23/// events: Vec<UserEvent>
24/// }
25///
26/// impl User{
27/// pub fn update_name(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
28/// let name = new_name.into();
29/// idempotency_guard!(
30/// self.events.iter().rev(),
31/// UserEvent::NameUpdated { name: existing_name } if existing_name == &name
32/// // above line returns early if same name found
33/// );
34/// self.events.push(UserEvent::NameUpdated{name});
35/// Idempotent::Executed(())
36/// }
37///
38/// pub fn update_name_with_break(&mut self, new_name: impl Into<String>) -> Idempotent<()>{
39/// let name = new_name.into();
40/// idempotency_guard!(
41/// self.events.iter().rev(),
42/// UserEvent::NameUpdated { name: existing_name } if existing_name == &name,
43/// => UserEvent::NameUpdated {..}
44/// // above line breaks iteration if same event found
45/// );
46/// self.events.push(UserEvent::NameUpdated{name});
47/// Idempotent::Executed(())
48/// }
49/// }
50///
51/// let mut user1 = User{ events: vec![] };
52/// let mut user2 = User{ events: vec![] };
53/// assert!(user1.update_name("Alice").did_execute());
54/// // updating "ALice" again ignored because same event with same name exists
55/// assert!(user1.update_name("Alice").was_already_applied());
56///
57/// assert!(user2.update_name_with_break("Alice").did_execute());
58/// assert!(user2.update_name_with_break("Bob").did_execute());
59/// // updating "ALice" again works because of early break condition
60/// assert!(user2.update_name_with_break("Alice").did_execute());
61/// ```
62#[macro_export]
63macro_rules! idempotency_guard {
64 ($events:expr, $( $pattern:pat $(if $guard:expr)? ),+ $(,)?) => {
65 for event in $events {
66 match event {
67 $(
68 $pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),
69 )+
70 _ => {}
71 }
72 }
73 };
74 ($events:expr, $( $pattern:pat $(if $guard:expr)? ),+,
75 => $break_pattern:pat $(if $break_guard:expr)?) => {
76 for event in $events {
77 match event {
78 $($pattern $(if $guard)? => return $crate::FromAlreadyApplied::from_already_applied(),)+
79 $break_pattern $(if $break_guard)? => break,
80 _ => {}
81 }
82 }
83 };
84}
85
86/// Execute an event-sourced query with automatic entity hydration.
87///
88/// Executes user-defined queries and returns entities by internally
89/// joining with events table to hydrate entities, essentially giving the
90/// illusion of working with just the index table.
91///
92/// **Important**: This macro only works inside functions (`fn`) that are defined
93/// within structs that have `#[derive(EsRepo)]` applied. The macro relies on
94/// the repository context to properly hydrate entities.
95///
96/// # Returns
97///
98/// Returns an [`EsQuery`](crate::query::EsQuery) struct that provides methods
99/// like [`fetch_optional()`](crate::query::EsQuery::fetch_optional) and
100/// [`fetch_n()`](crate::query::EsQuery::fetch_n) for executing the
101/// query and retrieving hydrated entities.
102///
103/// # Parameters
104///
105/// - `tbl_prefix`: Table prefix to ignore when deriving entity names from table names (optional)
106/// - `entity`: Override the entity type (optional, useful when table name doesn't match entity name)
107/// - SQL query string
108/// - Additional arguments for the SQL query (optional)
109///
110/// # Examples
111/// ```ignore
112/// // Basic usage
113/// es_query!("SELECT id FROM users WHERE id = $1", id)
114///
115/// // With table prefix
116/// es_query!(
117/// tbl_prefix = "app",
118/// "SELECT id FROM app_users WHERE active = true"
119/// )
120///
121/// // With custom entity type
122/// es_query!(
123/// entity = User,
124/// "SELECT id FROM custom_users_table WHERE id = $1",
125/// id as UserId
126/// )
127/// ```
128#[macro_export]
129macro_rules! es_query {
130 // With entity override
131 (
132 entity = $entity:ident,
133 $query:expr,
134 $($args:tt)*
135 ) => ({
136 $crate::expand_es_query!(
137 entity = $entity,
138 sql = $query,
139 args = [$($args)*]
140 )
141 });
142 // With entity override - no args
143 (
144 entity = $entity:ident,
145 $query:expr
146 ) => ({
147 $crate::expand_es_query!(
148 entity = $entity,
149 sql = $query
150 )
151 });
152
153 // With tbl_prefix
154 (
155 tbl_prefix = $tbl_prefix:literal,
156 $query:expr,
157 $($args:tt)*
158 ) => ({
159 $crate::expand_es_query!(
160 tbl_prefix = $tbl_prefix,
161 sql = $query,
162 args = [$($args)*]
163 )
164 });
165 // With tbl_prefix - no args
166 (
167 tbl_prefix = $tbl_prefix:literal,
168 $query:expr
169 ) => ({
170 $crate::expand_es_query!(
171 tbl_prefix = $tbl_prefix,
172 sql = $query
173 )
174 });
175
176 // Basic form
177 (
178 $query:expr,
179 $($args:tt)*
180 ) => ({
181 $crate::expand_es_query!(
182 sql = $query,
183 args = [$($args)*]
184 )
185 });
186 // Basic form - no args
187 (
188 $query:expr
189 ) => ({
190 $crate::expand_es_query!(
191 sql = $query
192 )
193 });
194}
195
196// Helper macro for common entity_id implementations (internal use only)
197#[doc(hidden)]
198#[macro_export]
199macro_rules! __entity_id_common_impls {
200 ($name:ident) => {
201 impl $name {
202 #[allow(clippy::new_without_default)]
203 pub fn new() -> Self {
204 $crate::prelude::uuid::Uuid::now_v7().into()
205 }
206 }
207
208 impl From<$crate::prelude::uuid::Uuid> for $name {
209 fn from(uuid: $crate::prelude::uuid::Uuid) -> Self {
210 Self(uuid)
211 }
212 }
213
214 impl From<$name> for $crate::prelude::uuid::Uuid {
215 fn from(id: $name) -> Self {
216 id.0
217 }
218 }
219
220 impl From<&$name> for $crate::prelude::uuid::Uuid {
221 fn from(id: &$name) -> Self {
222 id.0
223 }
224 }
225
226 impl std::fmt::Display for $name {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "{}", self.0)
229 }
230 }
231
232 impl std::str::FromStr for $name {
233 type Err = $crate::prelude::uuid::Error;
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 Ok(Self($crate::prelude::uuid::Uuid::parse_str(s)?))
237 }
238 }
239 };
240}
241
242// Helper macro for GraphQL-specific entity_id implementations (internal use only)
243#[doc(hidden)]
244#[macro_export]
245macro_rules! __entity_id_graphql_impls {
246 ($name:ident) => {
247 impl From<$crate::graphql::UUID> for $name {
248 fn from(id: $crate::graphql::UUID) -> Self {
249 $name($crate::prelude::uuid::Uuid::from(&id))
250 }
251 }
252
253 impl From<&$crate::graphql::UUID> for $name {
254 fn from(id: &$crate::graphql::UUID) -> Self {
255 $name($crate::prelude::uuid::Uuid::from(id))
256 }
257 }
258 };
259}
260
261// Helper macro for additional conversions (internal use only)
262#[doc(hidden)]
263#[macro_export]
264macro_rules! __entity_id_conversions {
265 ($($from:ty => $to:ty),* $(,)?) => {
266 $(
267 impl From<$from> for $to {
268 fn from(id: $from) -> Self {
269 <$to>::from($crate::prelude::uuid::Uuid::from(id))
270 }
271 }
272 impl From<$to> for $from {
273 fn from(id: $to) -> Self {
274 <$from>::from($crate::prelude::uuid::Uuid::from(id))
275 }
276 }
277 )*
278 };
279}
280
281#[doc(hidden)]
282#[cfg(all(feature = "graphql", feature = "json-schema"))]
283#[macro_export]
284macro_rules! entity_id {
285 // Match identifiers without conversions
286 ($($name:ident),+ $(,)?) => {
287 $crate::entity_id! { $($name),+ ; }
288 };
289 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
290 $(
291 #[derive(
292 $crate::prelude::sqlx::Type,
293 Debug,
294 Clone,
295 Copy,
296 PartialEq,
297 Eq,
298 PartialOrd,
299 Ord,
300 Hash,
301 $crate::prelude::serde::Deserialize,
302 $crate::prelude::serde::Serialize,
303 $crate::prelude::schemars::JsonSchema,
304 )]
305 #[schemars(crate = "es_entity::prelude::schemars")]
306 #[serde(crate = "es_entity::prelude::serde")]
307 #[serde(transparent)]
308 #[sqlx(transparent)]
309 pub struct $name($crate::prelude::uuid::Uuid);
310 $crate::__entity_id_common_impls!($name);
311 $crate::__entity_id_graphql_impls!($name);
312 )+
313 $crate::__entity_id_conversions!($($from => $to),*);
314 };
315}
316
317#[doc(hidden)]
318#[cfg(all(feature = "graphql", not(feature = "json-schema")))]
319#[macro_export]
320macro_rules! entity_id {
321 // Match identifiers without conversions
322 ($($name:ident),+ $(,)?) => {
323 $crate::entity_id! { $($name),+ ; }
324 };
325 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
326 $(
327 #[derive(
328 $crate::prelude::sqlx::Type,
329 Debug,
330 Clone,
331 Copy,
332 PartialEq,
333 Eq,
334 PartialOrd,
335 Ord,
336 Hash,
337 $crate::prelude::serde::Deserialize,
338 $crate::prelude::serde::Serialize,
339 )]
340 #[serde(crate = "es_entity::prelude::serde")]
341 #[serde(transparent)]
342 #[sqlx(transparent)]
343 pub struct $name($crate::prelude::uuid::Uuid);
344 $crate::__entity_id_common_impls!($name);
345 $crate::__entity_id_graphql_impls!($name);
346 )+
347 $crate::__entity_id_conversions!($($from => $to),*);
348 };
349}
350
351#[doc(hidden)]
352#[cfg(all(feature = "json-schema", not(feature = "graphql")))]
353#[macro_export]
354macro_rules! entity_id {
355 // Match identifiers without conversions
356 ($($name:ident),+ $(,)?) => {
357 $crate::entity_id! { $($name),+ ; }
358 };
359 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
360 $(
361 #[derive(
362 $crate::prelude::sqlx::Type,
363 Debug,
364 Clone,
365 Copy,
366 PartialEq,
367 Eq,
368 PartialOrd,
369 Ord,
370 Hash,
371 $crate::prelude::serde::Deserialize,
372 $crate::prelude::serde::Serialize,
373 $crate::prelude::schemars::JsonSchema,
374 )]
375 #[schemars(crate = "es_entity::prelude::schemars")]
376 #[serde(crate = "es_entity::prelude::serde")]
377 #[serde(transparent)]
378 #[sqlx(transparent)]
379 pub struct $name($crate::prelude::uuid::Uuid);
380 $crate::__entity_id_common_impls!($name);
381 )+
382 $crate::__entity_id_conversions!($($from => $to),*);
383 };
384}
385
386/// Create UUID-wrappers for database operations.
387///
388/// This macro generates type-safe UUID-wrapper structs with trait support for
389/// serialization, database operations, GraphQL integration, and JSON schema generation.
390///
391/// # Features
392///
393/// The macro automatically includes different trait implementations based on enabled features:
394/// - `graphql`: Adds GraphQL UUID conversion traits
395/// - `json-schema`: Adds JSON schema generation support
396///
397/// # Generated Traits
398///
399/// All entity IDs automatically implement:
400/// - `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
401/// - `serde::Serialize`, `serde::Deserialize` (with transparent serialization)
402/// - `sqlx::Type` (with transparent database type)
403/// - `Display` and `FromStr` for string conversion
404/// - `From<Uuid>` and `From<EntityId>` for UUID conversion
405///
406/// # Parameters
407///
408/// - `$name`: One or more entity ID type names to create
409/// - `$from => $to`: Optional conversion pairs between different entity ID types
410///
411/// # Examples
412///
413/// ```rust
414/// use es_entity::entity_id;
415///
416/// entity_id! { UserId, OrderId }
417///
418/// // Creates:
419/// // pub struct UserId(Uuid);
420/// // pub struct OrderId(Uuid);
421/// ```
422///
423/// ```rust
424/// use es_entity::entity_id;
425///
426/// entity_id! {
427/// UserId,
428/// AdminUserId;
429/// UserId => AdminUserId
430/// }
431///
432/// // Creates UserId and AdminUserId with `impl From` conversion between them
433/// ```
434#[cfg(all(not(feature = "json-schema"), not(feature = "graphql")))]
435#[macro_export]
436macro_rules! entity_id {
437 // Match identifiers without conversions
438 ($($name:ident),+ $(,)?) => {
439 $crate::entity_id! { $($name),+ ; }
440 };
441 ($($name:ident),+ $(,)? ; $($from:ty => $to:ty),* $(,)?) => {
442 $(
443 #[derive(
444 $crate::prelude::sqlx::Type,
445 Debug,
446 Clone,
447 Copy,
448 PartialEq,
449 Eq,
450 PartialOrd,
451 Ord,
452 Hash,
453 $crate::prelude::serde::Deserialize,
454 $crate::prelude::serde::Serialize,
455 )]
456 #[serde(crate = "es_entity::prelude::serde")]
457 #[serde(transparent)]
458 #[sqlx(transparent)]
459 pub struct $name($crate::prelude::uuid::Uuid);
460 $crate::__entity_id_common_impls!($name);
461 )+
462 $crate::__entity_id_conversions!($($from => $to),*);
463 };
464}