1#[macro_export]
22macro_rules! impl_entity_multi_tenant {
23 ($type:ident) => {
24 impl $type {
28 #[allow(dead_code)]
30 pub fn get_tenant_id(&self) -> ::uuid::Uuid {
31 self.tenant_id
32 }
33 }
34 };
35}
36
37#[macro_export]
41macro_rules! entity_fields {
42 () => {
43 pub id: ::uuid::Uuid,
45
46 #[serde(rename = "type")]
48 pub entity_type: String,
49
50 pub created_at: ::chrono::DateTime<::chrono::Utc>,
52
53 pub updated_at: ::chrono::DateTime<::chrono::Utc>,
55
56 pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
58
59 pub status: String,
61 };
62}
63
64#[macro_export]
66macro_rules! data_fields {
67 () => {
68 pub id: ::uuid::Uuid,
70
71 #[serde(rename = "type")]
73 pub entity_type: String,
74
75 pub created_at: ::chrono::DateTime<::chrono::Utc>,
77
78 pub updated_at: ::chrono::DateTime<::chrono::Utc>,
80
81 pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
83
84 pub status: String,
86
87 pub name: String,
89 };
90}
91
92#[macro_export]
94macro_rules! link_fields {
95 () => {
96 pub id: ::uuid::Uuid,
98
99 #[serde(rename = "type")]
101 pub entity_type: String,
102
103 pub created_at: ::chrono::DateTime<::chrono::Utc>,
105
106 pub updated_at: ::chrono::DateTime<::chrono::Utc>,
108
109 pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
111
112 pub status: String,
114
115 pub link_type: String,
117
118 pub source_id: ::uuid::Uuid,
120
121 pub target_id: ::uuid::Uuid,
123 };
124}
125
126#[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 pub id: ::uuid::Uuid,
167
168 #[serde(rename = "type")]
170 pub entity_type: String,
171
172 pub created_at: ::chrono::DateTime<::chrono::Utc>,
174
175 pub updated_at: ::chrono::DateTime<::chrono::Utc>,
177
178 pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
180
181 pub status: String,
183
184 pub name: String,
186 $( pub $specific_field : $specific_type ),*
187 }
188
189 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 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 impl $type {
260 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 pub fn soft_delete(&mut self) {
280 self.deleted_at = Some(::chrono::Utc::now());
281 self.updated_at = ::chrono::Utc::now();
282 }
283
284 pub fn restore(&mut self) {
286 self.deleted_at = None;
287 self.updated_at = ::chrono::Utc::now();
288 }
289
290 pub fn touch(&mut self) {
292 self.updated_at = ::chrono::Utc::now();
293 }
294
295 pub fn set_status(&mut self, status: String) {
297 self.status = status;
298 self.touch();
299 }
300 }
301 };
302}
303
304#[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 pub id: ::uuid::Uuid,
343
344 #[serde(rename = "type")]
346 pub entity_type: String,
347
348 pub created_at: ::chrono::DateTime<::chrono::Utc>,
350
351 pub updated_at: ::chrono::DateTime<::chrono::Utc>,
353
354 pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
356
357 pub status: String,
359
360 pub link_type: String,
362
363 pub source_id: ::uuid::Uuid,
365
366 pub target_id: ::uuid::Uuid,
368 $( pub $specific_field : $specific_type ),*
369 }
370
371 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 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 impl $type {
438 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 pub fn soft_delete(&mut self) {
462 self.deleted_at = Some(::chrono::Utc::now());
463 self.updated_at = ::chrono::Utc::now();
464 }
465
466 #[allow(dead_code)]
468 pub fn restore(&mut self) {
469 self.deleted_at = None;
470 self.updated_at = ::chrono::Utc::now();
471 }
472
473 #[allow(dead_code)]
475 pub fn touch(&mut self) {
476 self.updated_at = ::chrono::Utc::now();
477 }
478
479 #[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#[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 $crate::impl_data_entity!(
557 $type,
558 $type_name,
559 [ $( $indexed_field ),* ],
560 {
561 $( $specific_field : $specific_type ),*
562 }
563 );
564
565 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 $(
574 if operation == stringify!($op) {
575 $(
576 $crate::add_validators_for_field!(config, stringify!($val_field), $( $validator )*);
578 )*
579 }
580 )*
581
582 $(
584 if operation == stringify!($fop) {
585 $(
586 $crate::add_filters_for_field!(config, stringify!($fil_field), $( $filter )*);
588 )*
589 }
590 )*
591
592 config
593 }
594 }
595 };
596}
597
598#[macro_export]
600macro_rules! add_validators_for_field {
601 ($config:expr, $field:expr,) => {};
603
604 ($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 ($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 ($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 ($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 ($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 ($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 ($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#[macro_export]
649macro_rules! add_filters_for_field {
650 ($config:expr, $field:expr,) => {};
652
653 ($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 ($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 ($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 ($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 impl_data_entity!(
684 TestUser,
685 "test_user",
686 ["name", "email"],
687 {
688 email: String,
689 }
690 );
691
692 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}