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/// Extended macro to create a Data entity with validation and filtering
490///
491/// This macro extends `impl_data_entity!` with declarative validation and filtering support.
492///
493/// # Example
494///
495/// ```rust,ignore
496/// use this::prelude::*;
497///
498/// impl_data_entity_validated!(
499///     Invoice,
500///     "invoice",
501///     ["name", "number"],
502///     {
503///         number: String,
504///         amount: f64,
505///         due_date: Option<String>,
506///     },
507///     validate: {
508///         create: {
509///             number: [required, string_length(3, 50)],
510///             amount: [required, positive],
511///         },
512///         update: {
513///             amount: [optional, positive],
514///         },
515///     },
516///     filters: {
517///         create: {
518///             number: [trim, uppercase],
519///             amount: [round_decimals(2)],
520///         },
521///     }
522/// );
523/// ```
524#[macro_export]
525macro_rules! impl_data_entity_validated {
526    (
527        $type:ident,
528        $type_name:expr,
529        [ $( $indexed_field:expr ),* $(,)? ],
530        {
531            $( $specific_field:ident : $specific_type:ty ),* $(,)?
532        }
533        $(,)?
534        validate: {
535            $(
536                $op:ident: {
537                    $(
538                        $val_field:ident: [ $( $validator:tt )* ]
539                    ),* $(,)?
540                }
541            ),* $(,)?
542        }
543        $(,)?
544        filters: {
545            $(
546                $fop:ident: {
547                    $(
548                        $fil_field:ident: [ $( $filter:tt )* ]
549                    ),* $(,)?
550                }
551            ),* $(,)?
552        }
553        $(,)?
554    ) => {
555        // 1. Generate the base entity (reuse existing macro)
556        $crate::impl_data_entity!(
557            $type,
558            $type_name,
559            [ $( $indexed_field ),* ],
560            {
561                $( $specific_field : $specific_type ),*
562            }
563        );
564
565        // 2. Implement ValidatableEntity trait for validation support
566        impl $crate::core::validation::extractor::ValidatableEntity for $type {
567            fn validation_config(operation: &str) -> $crate::core::validation::EntityValidationConfig {
568                use $crate::core::validation::*;
569
570                let mut config = EntityValidationConfig::new($type_name);
571
572                // Generate validation rules per operation
573                $(
574                    if operation == stringify!($op) {
575                        $(
576                            // Add validators for each field
577                            $crate::add_validators_for_field!(config, stringify!($val_field), $( $validator )*);
578                        )*
579                    }
580                )*
581
582                // Generate filters per operation
583                $(
584                    if operation == stringify!($fop) {
585                        $(
586                            // Add filters for each field
587                            $crate::add_filters_for_field!(config, stringify!($fil_field), $( $filter )*);
588                        )*
589                    }
590                )*
591
592                config
593            }
594        }
595    };
596}
597
598/// Helper macro to add validators to a field
599#[macro_export]
600macro_rules! add_validators_for_field {
601    // Base case: empty
602    ($config:expr, $field:expr,) => {};
603
604    // required
605    ($config:expr, $field:expr, required $( $rest:tt )*) => {
606        $config.add_validator($field, $crate::core::validation::validators::required());
607        $crate::add_validators_for_field!($config, $field, $( $rest )*);
608    };
609
610    // optional
611    ($config:expr, $field:expr, optional $( $rest:tt )*) => {
612        $config.add_validator($field, $crate::core::validation::validators::optional());
613        $crate::add_validators_for_field!($config, $field, $( $rest )*);
614    };
615
616    // positive
617    ($config:expr, $field:expr, positive $( $rest:tt )*) => {
618        $config.add_validator($field, $crate::core::validation::validators::positive());
619        $crate::add_validators_for_field!($config, $field, $( $rest )*);
620    };
621
622    // string_length with parameters
623    ($config:expr, $field:expr, string_length($min:expr, $max:expr) $( $rest:tt )*) => {
624        $config.add_validator($field, $crate::core::validation::validators::string_length($min, $max));
625        $crate::add_validators_for_field!($config, $field, $( $rest )*);
626    };
627
628    // max_value with parameter
629    ($config:expr, $field:expr, max_value($max:expr) $( $rest:tt )*) => {
630        $config.add_validator($field, $crate::core::validation::validators::max_value($max));
631        $crate::add_validators_for_field!($config, $field, $( $rest )*);
632    };
633
634    // in_list with values
635    ($config:expr, $field:expr, in_list($( $value:expr ),* $(,)?) $( $rest:tt )*) => {
636        $config.add_validator($field, $crate::core::validation::validators::in_list(vec![$( $value.to_string() ),*]));
637        $crate::add_validators_for_field!($config, $field, $( $rest )*);
638    };
639
640    // date_format with format string
641    ($config:expr, $field:expr, date_format($format:expr) $( $rest:tt )*) => {
642        $config.add_validator($field, $crate::core::validation::validators::date_format($format));
643        $crate::add_validators_for_field!($config, $field, $( $rest )*);
644    };
645}
646
647/// Helper macro to add filters to a field
648#[macro_export]
649macro_rules! add_filters_for_field {
650    // Base case: empty
651    ($config:expr, $field:expr,) => {};
652
653    // trim
654    ($config:expr, $field:expr, trim $( $rest:tt )*) => {
655        $config.add_filter($field, $crate::core::validation::filters::trim());
656        $crate::add_filters_for_field!($config, $field, $( $rest )*);
657    };
658
659    // uppercase
660    ($config:expr, $field:expr, uppercase $( $rest:tt )*) => {
661        $config.add_filter($field, $crate::core::validation::filters::uppercase());
662        $crate::add_filters_for_field!($config, $field, $( $rest )*);
663    };
664
665    // lowercase
666    ($config:expr, $field:expr, lowercase $( $rest:tt )*) => {
667        $config.add_filter($field, $crate::core::validation::filters::lowercase());
668        $crate::add_filters_for_field!($config, $field, $( $rest )*);
669    };
670
671    // round_decimals with parameter
672    ($config:expr, $field:expr, round_decimals($decimals:expr) $( $rest:tt )*) => {
673        $config.add_filter($field, $crate::core::validation::filters::round_decimals($decimals));
674        $crate::add_filters_for_field!($config, $field, $( $rest )*);
675    };
676}
677
678#[cfg(test)]
679mod tests {
680    use crate::prelude::*;
681
682    // Test Data entity
683    impl_data_entity!(
684        TestUser,
685        "test_user",
686        ["name", "email"],
687        {
688            email: String,
689        }
690    );
691
692    // Test Link entity
693    impl_link_entity!(
694        TestOwnerLink,
695        "test_owner_link",
696        {
697            since: DateTime<Utc>,
698        }
699    );
700
701    #[test]
702    fn test_data_entity_creation() {
703        let user = TestUser::new(
704            "John Doe".to_string(),
705            "active".to_string(),
706            "john@example.com".to_string(),
707        );
708
709        assert_eq!(user.name(), "John Doe");
710        assert_eq!(user.status(), "active");
711        assert_eq!(user.email, "john@example.com");
712        assert!(!user.is_deleted());
713        assert!(user.is_active());
714    }
715
716    #[test]
717    fn test_data_entity_soft_delete() {
718        let mut user = TestUser::new(
719            "John Doe".to_string(),
720            "active".to_string(),
721            "john@example.com".to_string(),
722        );
723
724        assert!(!user.is_deleted());
725        user.soft_delete();
726        assert!(user.is_deleted());
727        assert!(!user.is_active());
728    }
729
730    #[test]
731    fn test_data_entity_restore() {
732        let mut user = TestUser::new(
733            "John Doe".to_string(),
734            "active".to_string(),
735            "john@example.com".to_string(),
736        );
737
738        user.soft_delete();
739        assert!(user.is_deleted());
740
741        user.restore();
742        assert!(!user.is_deleted());
743        assert!(user.is_active());
744    }
745
746    #[test]
747    fn test_link_entity_creation() {
748        let user_id = Uuid::new_v4();
749        let car_id = Uuid::new_v4();
750
751        let link = TestOwnerLink::new(
752            "owner".to_string(),
753            user_id,
754            car_id,
755            "active".to_string(),
756            Utc::now(),
757        );
758
759        assert_eq!(link.source_id(), user_id);
760        assert_eq!(link.target_id(), car_id);
761        assert_eq!(link.link_type(), "owner");
762        assert_eq!(link.status(), "active");
763        assert!(!link.is_deleted());
764    }
765
766    #[test]
767    fn test_link_entity_soft_delete() {
768        let link = TestOwnerLink::new(
769            "owner".to_string(),
770            Uuid::new_v4(),
771            Uuid::new_v4(),
772            "active".to_string(),
773            Utc::now(),
774        );
775
776        let mut link = link;
777        assert!(!link.is_deleted());
778
779        link.soft_delete();
780        assert!(link.is_deleted());
781    }
782
783    #[test]
784    fn test_entity_set_status() {
785        let mut user = TestUser::new(
786            "John Doe".to_string(),
787            "active".to_string(),
788            "john@example.com".to_string(),
789        );
790
791        assert_eq!(user.status(), "active");
792
793        user.set_status("inactive".to_string());
794        assert_eq!(user.status(), "inactive");
795    }
796}