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