this/entities/
macros.rs

1//! Macros for reducing boilerplate when defining entities
2//!
3//! These macros generate the repetitive trait implementations needed
4//! for each entity type following the Entity/Data/Link architecture.
5
6/// Helper macro to enable multi-tenancy for an entity
7///
8/// This macro adds an override for the `Entity::tenant_id()` method
9/// to return the actual tenant_id field value.
10///
11/// # Example
12/// ```rust,ignore
13/// impl_data_entity!(User, "user", ["name"], {
14///     tenant_id: Uuid,
15///     email: String,
16/// });
17///
18/// // Enable multi-tenancy
19/// impl_entity_multi_tenant!(User);
20/// ```
21#[macro_export]
22macro_rules! impl_entity_multi_tenant {
23    ($type:ident) => {
24        // Cannot override trait methods in separate impl blocks in stable Rust
25        // This is a marker for documentation purposes
26        // Users should manually implement tenant_id access via a helper method
27        impl $type {
28            /// Get the tenant ID for multi-tenant isolation
29            #[allow(dead_code)]
30            pub fn get_tenant_id(&self) -> ::uuid::Uuid {
31                self.tenant_id
32            }
33        }
34    };
35}
36
37/// Macro to inject Entity base fields into a struct
38///
39/// Injects: id, entity_type, created_at, updated_at, deleted_at, status
40#[macro_export]
41macro_rules! entity_fields {
42    () => {
43        /// Unique identifier for this entity
44        pub id: ::uuid::Uuid,
45
46        /// Type of the entity (e.g., "user", "product")
47        #[serde(rename = "type")]
48        pub entity_type: String,
49
50        /// When this entity was created
51        pub created_at: ::chrono::DateTime<::chrono::Utc>,
52
53        /// When this entity was last updated
54        pub updated_at: ::chrono::DateTime<::chrono::Utc>,
55
56        /// When this entity was soft-deleted (if applicable)
57        pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
58
59        /// Current status of the entity
60        pub status: String,
61    };
62}
63
64/// Macro to inject Data fields into a struct (Entity fields + name)
65#[macro_export]
66macro_rules! data_fields {
67    () => {
68        /// Unique identifier for this entity
69        pub id: ::uuid::Uuid,
70
71        /// Type of the entity (e.g., "user", "product")
72        #[serde(rename = "type")]
73        pub entity_type: String,
74
75        /// When this entity was created
76        pub created_at: ::chrono::DateTime<::chrono::Utc>,
77
78        /// When this entity was last updated
79        pub updated_at: ::chrono::DateTime<::chrono::Utc>,
80
81        /// When this entity was soft-deleted (if applicable)
82        pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
83
84        /// Current status of the entity
85        pub status: String,
86
87        /// Name of this data entity
88        pub name: String,
89    };
90}
91
92/// Macro to inject Link fields into a struct (Entity fields + source_id + target_id + link_type)
93#[macro_export]
94macro_rules! link_fields {
95    () => {
96        /// Unique identifier for this entity
97        pub id: ::uuid::Uuid,
98
99        /// Type of the entity (e.g., "user", "product")
100        #[serde(rename = "type")]
101        pub entity_type: String,
102
103        /// When this entity was created
104        pub created_at: ::chrono::DateTime<::chrono::Utc>,
105
106        /// When this entity was last updated
107        pub updated_at: ::chrono::DateTime<::chrono::Utc>,
108
109        /// When this entity was soft-deleted (if applicable)
110        pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
111
112        /// Current status of the entity
113        pub status: String,
114
115        /// Type of relationship
116        pub link_type: String,
117
118        /// ID of the source entity
119        pub source_id: ::uuid::Uuid,
120
121        /// ID of the target entity
122        pub target_id: ::uuid::Uuid,
123    };
124}
125
126/// Complete macro to create a Data entity with automatic trait implementations
127///
128/// # Example
129///
130/// ```rust,ignore
131/// use this::prelude::*;
132///
133/// impl_data_entity!(
134///     User,
135///     "user",
136///     ["name", "email"],
137///     {
138///         email: String,
139///         password_hash: String,
140///         roles: Vec<String>,
141///     }
142/// );
143///
144/// // Usage
145/// let user = User::new(
146///     "John Doe".to_string(),
147///     "active".to_string(),
148///     "john@example.com".to_string(),
149///     "$argon2$...".to_string(),
150///     vec!["admin".to_string()],
151/// );
152/// ```
153#[macro_export]
154macro_rules! impl_data_entity {
155    (
156        $type:ident,
157        $type_name:expr,
158        [ $( $indexed_field:expr ),* $(,)? ],
159        {
160            $( $specific_field:ident : $specific_type:ty ),* $(,)?
161        }
162    ) => {
163        #[derive(Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
164        pub struct $type {
165            /// Unique identifier for this entity
166            pub id: ::uuid::Uuid,
167
168            /// Type of the entity
169            #[serde(rename = "type")]
170            pub entity_type: String,
171
172            /// When this entity was created
173            pub created_at: ::chrono::DateTime<::chrono::Utc>,
174
175            /// When this entity was last updated
176            pub updated_at: ::chrono::DateTime<::chrono::Utc>,
177
178            /// When this entity was soft-deleted (if applicable)
179            pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
180
181            /// Current status of the entity
182            pub status: String,
183
184            /// Name of this data entity
185            pub name: String,
186            $( pub $specific_field : $specific_type ),*
187        }
188
189        // Implement Entity trait
190        impl $crate::core::entity::Entity for $type {
191            type Service = ();
192
193            fn resource_name() -> &'static str {
194                use std::sync::OnceLock;
195                static PLURAL: OnceLock<&'static str> = OnceLock::new();
196                PLURAL.get_or_init(|| {
197                    Box::leak(
198                        $crate::core::pluralize::Pluralizer::pluralize($type_name)
199                            .into_boxed_str()
200                    )
201                })
202            }
203
204            fn resource_name_singular() -> &'static str {
205                $type_name
206            }
207
208            fn service_from_host(
209                _host: &::std::sync::Arc<dyn ::std::any::Any + Send + Sync>
210            ) -> ::anyhow::Result<::std::sync::Arc<Self::Service>> {
211                unimplemented!("service_from_host must be implemented by user")
212            }
213
214            fn id(&self) -> ::uuid::Uuid {
215                self.id
216            }
217
218            fn entity_type(&self) -> &str {
219                &self.entity_type
220            }
221
222            fn created_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
223                self.created_at
224            }
225
226            fn updated_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
227                self.updated_at
228            }
229
230            fn deleted_at(&self) -> Option<::chrono::DateTime<::chrono::Utc>> {
231                self.deleted_at
232            }
233
234            fn status(&self) -> &str {
235                &self.status
236            }
237        }
238
239        // Implement Data trait
240        impl $crate::core::entity::Data for $type {
241            fn name(&self) -> &str {
242                &self.name
243            }
244
245            fn indexed_fields() -> &'static [&'static str] {
246                &[ $( $indexed_field ),* ]
247            }
248
249            fn field_value(&self, field: &str) -> Option<$crate::core::field::FieldValue> {
250                match field {
251                    "name" => Some($crate::core::field::FieldValue::String(self.name.clone())),
252                    "status" => Some($crate::core::field::FieldValue::String(self.status.clone())),
253                    _ => None,
254                }
255            }
256        }
257
258        // Utility methods
259        impl $type {
260            /// Create a new instance of this entity
261            pub fn new(
262                name: String,
263                status: String,
264                $( $specific_field: $specific_type ),*
265            ) -> Self {
266                Self {
267                    id: ::uuid::Uuid::new_v4(),
268                    entity_type: $type_name.to_string(),
269                    created_at: ::chrono::Utc::now(),
270                    updated_at: ::chrono::Utc::now(),
271                    deleted_at: None,
272                    status,
273                    name,
274                    $( $specific_field ),*
275                }
276            }
277
278            /// Soft delete this entity (sets deleted_at timestamp)
279            pub fn soft_delete(&mut self) {
280                self.deleted_at = Some(::chrono::Utc::now());
281                self.updated_at = ::chrono::Utc::now();
282            }
283
284            /// Restore a soft-deleted entity (clears deleted_at timestamp)
285            pub fn restore(&mut self) {
286                self.deleted_at = None;
287                self.updated_at = ::chrono::Utc::now();
288            }
289
290            /// Update the updated_at timestamp to now
291            pub fn touch(&mut self) {
292                self.updated_at = ::chrono::Utc::now();
293            }
294
295            /// Change the entity status
296            pub fn set_status(&mut self, status: String) {
297                self.status = status;
298                self.touch();
299            }
300        }
301    };
302}
303
304/// Complete macro to create a Link entity with automatic trait implementations
305///
306/// # Example
307///
308/// ```rust,ignore
309/// use this::prelude::*;
310///
311/// impl_link_entity!(
312///     UserCompanyLink,
313///     "user_company_link",
314///     {
315///         role: String,
316///         start_date: DateTime<Utc>,
317///     }
318/// );
319///
320/// // Usage
321/// let link = UserCompanyLink::new(
322///     "employment".to_string(),
323///     user_id,
324///     company_id,
325///     "active".to_string(),
326///     "Senior Developer".to_string(),
327///     Utc::now(),
328/// );
329/// ```
330#[macro_export]
331macro_rules! impl_link_entity {
332    (
333        $type:ident,
334        $type_name:expr,
335        {
336            $( $specific_field:ident : $specific_type:ty ),* $(,)?
337        }
338    ) => {
339        #[derive(Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
340        pub struct $type {
341            /// Unique identifier for this entity
342            pub id: ::uuid::Uuid,
343
344            /// Type of the entity
345            #[serde(rename = "type")]
346            pub entity_type: String,
347
348            /// When this entity was created
349            pub created_at: ::chrono::DateTime<::chrono::Utc>,
350
351            /// When this entity was last updated
352            pub updated_at: ::chrono::DateTime<::chrono::Utc>,
353
354            /// When this entity was soft-deleted (if applicable)
355            pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
356
357            /// Current status of the entity
358            pub status: String,
359
360            /// Type of relationship
361            pub link_type: String,
362
363            /// ID of the source entity
364            pub source_id: ::uuid::Uuid,
365
366            /// ID of the target entity
367            pub target_id: ::uuid::Uuid,
368            $( pub $specific_field : $specific_type ),*
369        }
370
371        // Implement Entity trait
372        impl $crate::core::entity::Entity for $type {
373            type Service = ();
374
375            fn resource_name() -> &'static str {
376                use std::sync::OnceLock;
377                static PLURAL: OnceLock<&'static str> = OnceLock::new();
378                PLURAL.get_or_init(|| {
379                    Box::leak(
380                        $crate::core::pluralize::Pluralizer::pluralize($type_name)
381                            .into_boxed_str()
382                    )
383                })
384            }
385
386            fn resource_name_singular() -> &'static str {
387                $type_name
388            }
389
390            fn service_from_host(
391                _host: &::std::sync::Arc<dyn ::std::any::Any + Send + Sync>
392            ) -> ::anyhow::Result<::std::sync::Arc<Self::Service>> {
393                unimplemented!("service_from_host must be implemented by user")
394            }
395
396            fn id(&self) -> ::uuid::Uuid {
397                self.id
398            }
399
400            fn entity_type(&self) -> &str {
401                &self.entity_type
402            }
403
404            fn created_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
405                self.created_at
406            }
407
408            fn updated_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
409                self.updated_at
410            }
411
412            fn deleted_at(&self) -> Option<::chrono::DateTime<::chrono::Utc>> {
413                self.deleted_at
414            }
415
416            fn status(&self) -> &str {
417                &self.status
418            }
419        }
420
421        // Implement Link trait
422        impl $crate::core::entity::Link for $type {
423            fn source_id(&self) -> ::uuid::Uuid {
424                self.source_id
425            }
426
427            fn target_id(&self) -> ::uuid::Uuid {
428                self.target_id
429            }
430
431            fn link_type(&self) -> &str {
432                &self.link_type
433            }
434        }
435
436        // Utility methods
437        impl $type {
438            /// Create a new link instance
439            pub fn new(
440                link_type: String,
441                source_id: ::uuid::Uuid,
442                target_id: ::uuid::Uuid,
443                status: String,
444                $( $specific_field: $specific_type ),*
445            ) -> Self {
446                Self {
447                    id: ::uuid::Uuid::new_v4(),
448                    entity_type: $type_name.to_string(),
449                    created_at: ::chrono::Utc::now(),
450                    updated_at: ::chrono::Utc::now(),
451                    deleted_at: None,
452                    status,
453                    link_type,
454                    source_id,
455                    target_id,
456                    $( $specific_field ),*
457                }
458            }
459
460            /// Soft delete this link
461            pub fn soft_delete(&mut self) {
462                self.deleted_at = Some(::chrono::Utc::now());
463                self.updated_at = ::chrono::Utc::now();
464            }
465
466            /// Restore a soft-deleted link
467            #[allow(dead_code)]
468            pub fn restore(&mut self) {
469                self.deleted_at = None;
470                self.updated_at = ::chrono::Utc::now();
471            }
472
473            /// Update the updated_at timestamp
474            #[allow(dead_code)]
475            pub fn touch(&mut self) {
476                self.updated_at = ::chrono::Utc::now();
477            }
478
479            /// Change the link status
480            #[allow(dead_code)]
481            pub fn set_status(&mut self, status: String) {
482                self.status = status;
483                self.touch();
484            }
485        }
486    };
487}
488
489#[cfg(test)]
490mod tests {
491    use crate::prelude::*;
492
493    // Test Data entity
494    impl_data_entity!(
495        TestUser,
496        "test_user",
497        ["name", "email"],
498        {
499            email: String,
500        }
501    );
502
503    // Test Link entity
504    impl_link_entity!(
505        TestOwnerLink,
506        "test_owner_link",
507        {
508            since: DateTime<Utc>,
509        }
510    );
511
512    #[test]
513    fn test_data_entity_creation() {
514        let user = TestUser::new(
515            "John Doe".to_string(),
516            "active".to_string(),
517            "john@example.com".to_string(),
518        );
519
520        assert_eq!(user.name(), "John Doe");
521        assert_eq!(user.status(), "active");
522        assert_eq!(user.email, "john@example.com");
523        assert!(!user.is_deleted());
524        assert!(user.is_active());
525    }
526
527    #[test]
528    fn test_data_entity_soft_delete() {
529        let mut user = TestUser::new(
530            "John Doe".to_string(),
531            "active".to_string(),
532            "john@example.com".to_string(),
533        );
534
535        assert!(!user.is_deleted());
536        user.soft_delete();
537        assert!(user.is_deleted());
538        assert!(!user.is_active());
539    }
540
541    #[test]
542    fn test_data_entity_restore() {
543        let mut user = TestUser::new(
544            "John Doe".to_string(),
545            "active".to_string(),
546            "john@example.com".to_string(),
547        );
548
549        user.soft_delete();
550        assert!(user.is_deleted());
551
552        user.restore();
553        assert!(!user.is_deleted());
554        assert!(user.is_active());
555    }
556
557    #[test]
558    fn test_link_entity_creation() {
559        let user_id = Uuid::new_v4();
560        let car_id = Uuid::new_v4();
561
562        let link = TestOwnerLink::new(
563            "owner".to_string(),
564            user_id,
565            car_id,
566            "active".to_string(),
567            Utc::now(),
568        );
569
570        assert_eq!(link.source_id(), user_id);
571        assert_eq!(link.target_id(), car_id);
572        assert_eq!(link.link_type(), "owner");
573        assert_eq!(link.status(), "active");
574        assert!(!link.is_deleted());
575    }
576
577    #[test]
578    fn test_link_entity_soft_delete() {
579        let link = TestOwnerLink::new(
580            "owner".to_string(),
581            Uuid::new_v4(),
582            Uuid::new_v4(),
583            "active".to_string(),
584            Utc::now(),
585        );
586
587        let mut link = link;
588        assert!(!link.is_deleted());
589
590        link.soft_delete();
591        assert!(link.is_deleted());
592    }
593
594    #[test]
595    fn test_entity_set_status() {
596        let mut user = TestUser::new(
597            "John Doe".to_string(),
598            "active".to_string(),
599            "john@example.com".to_string(),
600        );
601
602        assert_eq!(user.status(), "active");
603
604        user.set_status("inactive".to_string());
605        assert_eq!(user.status(), "inactive");
606    }
607}