Skip to main content

this/core/
entity.rs

1//! Entity traits defining the core abstraction for all data types
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use std::sync::Arc;
6use uuid::Uuid;
7
8/// Base trait for all entities in the system.
9///
10/// This trait provides the fundamental metadata needed for any entity type.
11/// All entities have:
12/// - id: Unique identifier
13/// - type: Entity type name (e.g., "user", "product")
14/// - created_at: Creation timestamp
15/// - updated_at: Last modification timestamp
16/// - deleted_at: Soft deletion timestamp (optional)
17/// - status: Current status of the entity
18pub trait Entity: Clone + Send + Sync + 'static {
19    /// The service type that handles operations for this entity
20    type Service: Send + Sync;
21
22    /// The plural resource name used in URLs (e.g., "users", "companies")
23    fn resource_name() -> &'static str;
24
25    /// The singular resource name (e.g., "user", "company")
26    fn resource_name_singular() -> &'static str;
27
28    /// Extract the service instance from the application host/state
29    fn service_from_host(host: &Arc<dyn std::any::Any + Send + Sync>)
30    -> Result<Arc<Self::Service>>;
31
32    // === Core Entity Fields ===
33
34    /// Get the unique identifier for this entity instance
35    fn id(&self) -> Uuid;
36
37    /// Get the entity type name
38    fn entity_type(&self) -> &str;
39
40    /// Get the creation timestamp
41    fn created_at(&self) -> DateTime<Utc>;
42
43    /// Get the last update timestamp
44    fn updated_at(&self) -> DateTime<Utc>;
45
46    /// Get the deletion timestamp (soft delete)
47    fn deleted_at(&self) -> Option<DateTime<Utc>>;
48
49    /// Get the entity status
50    fn status(&self) -> &str;
51
52    // === Utility Methods ===
53
54    /// Get the tenant ID for multi-tenant isolation.
55    ///
56    /// Returns None by default for single-tenant applications or system-wide entities.
57    /// Override this method to enable multi-tenancy for specific entity types.
58    ///
59    /// # Multi-Tenant Usage
60    ///
61    /// ```rust,ignore
62    /// impl Entity for MyEntity {
63    ///     fn tenant_id(&self) -> Option<Uuid> {
64    ///         self.tenant_id  // Return actual tenant_id field
65    ///     }
66    /// }
67    /// ```
68    fn tenant_id(&self) -> Option<Uuid> {
69        None
70    }
71
72    /// Check if the entity has been soft-deleted
73    fn is_deleted(&self) -> bool {
74        self.deleted_at().is_some()
75    }
76
77    /// Check if the entity is active (status == "active" and not deleted)
78    fn is_active(&self) -> bool {
79        self.status() == "active" && !self.is_deleted()
80    }
81}
82
83/// Trait for data entities that represent concrete domain objects.
84///
85/// Data entities extend the base Entity with:
86/// - name: A human-readable name
87/// - indexed_fields: Fields that can be searched
88/// - field_value: Dynamic field access
89pub trait Data: Entity {
90    /// Get the name of this data entity
91    fn name(&self) -> &str;
92
93    /// List of fields that should be indexed for searching
94    fn indexed_fields() -> &'static [&'static str];
95
96    /// Get the value of a specific field by name
97    fn field_value(&self, field: &str) -> Option<crate::core::field::FieldValue>;
98
99    /// Display the entity for debugging
100    fn display(&self) {
101        println!(
102            "[{}] {} - {} ({})",
103            self.id(),
104            self.entity_type(),
105            self.name(),
106            self.status()
107        );
108    }
109}
110
111/// Trait for link entities that represent relationships between entities.
112///
113/// Links extend the base Entity with:
114/// - source_id: The ID of the source entity
115/// - target_id: The ID of the target entity
116/// - link_type: The type of relationship
117pub trait Link: Entity {
118    /// Get the source entity ID
119    fn source_id(&self) -> Uuid;
120
121    /// Get the target entity ID
122    fn target_id(&self) -> Uuid;
123
124    /// Get the link type (e.g., "owner", "worker")
125    fn link_type(&self) -> &str;
126
127    /// Display the link for debugging
128    fn display(&self) {
129        println!(
130            "[{}] {} → {} (type: {}, status: {})",
131            self.id(),
132            self.source_id(),
133            self.target_id(),
134            self.link_type(),
135            self.status()
136        );
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use serde::{Deserialize, Serialize};
144
145    // Example entity for testing trait definitions
146    #[derive(Clone, Debug, Serialize, Deserialize)]
147    struct TestEntity {
148        id: Uuid,
149        entity_type: String,
150        created_at: DateTime<Utc>,
151        updated_at: DateTime<Utc>,
152        deleted_at: Option<DateTime<Utc>>,
153        status: String,
154    }
155
156    impl Entity for TestEntity {
157        type Service = ();
158
159        fn resource_name() -> &'static str {
160            "test_entities"
161        }
162
163        fn resource_name_singular() -> &'static str {
164            "test_entity"
165        }
166
167        fn service_from_host(
168            _host: &Arc<dyn std::any::Any + Send + Sync>,
169        ) -> Result<Arc<Self::Service>> {
170            Ok(Arc::new(()))
171        }
172
173        fn id(&self) -> Uuid {
174            self.id
175        }
176
177        fn entity_type(&self) -> &str {
178            &self.entity_type
179        }
180
181        fn created_at(&self) -> DateTime<Utc> {
182            self.created_at
183        }
184
185        fn updated_at(&self) -> DateTime<Utc> {
186            self.updated_at
187        }
188
189        fn deleted_at(&self) -> Option<DateTime<Utc>> {
190            self.deleted_at
191        }
192
193        fn status(&self) -> &str {
194            &self.status
195        }
196    }
197
198    #[test]
199    fn test_entity_is_deleted() {
200        let now = Utc::now();
201        let mut entity = TestEntity {
202            id: Uuid::new_v4(),
203            entity_type: "test".to_string(),
204            created_at: now,
205            updated_at: now,
206            deleted_at: None,
207            status: "active".to_string(),
208        };
209
210        assert!(!entity.is_deleted());
211        assert!(entity.is_active());
212
213        entity.deleted_at = Some(now);
214        assert!(entity.is_deleted());
215        assert!(!entity.is_active());
216    }
217
218    #[test]
219    fn test_entity_metadata() {
220        assert_eq!(TestEntity::resource_name(), "test_entities");
221        assert_eq!(TestEntity::resource_name_singular(), "test_entity");
222    }
223
224    #[test]
225    fn test_entity_default_tenant_id_is_none() {
226        let now = Utc::now();
227        let entity = TestEntity {
228            id: Uuid::new_v4(),
229            entity_type: "test".to_string(),
230            created_at: now,
231            updated_at: now,
232            deleted_at: None,
233            status: "active".to_string(),
234        };
235        assert_eq!(entity.tenant_id(), None);
236    }
237
238    #[test]
239    fn test_entity_is_active_with_inactive_status() {
240        let now = Utc::now();
241        let entity = TestEntity {
242            id: Uuid::new_v4(),
243            entity_type: "test".to_string(),
244            created_at: now,
245            updated_at: now,
246            deleted_at: None,
247            status: "inactive".to_string(),
248        };
249        assert!(!entity.is_active());
250        assert!(!entity.is_deleted());
251    }
252
253    #[test]
254    fn test_entity_service_from_host() {
255        let host: Arc<dyn std::any::Any + Send + Sync> = Arc::new(());
256        let svc = TestEntity::service_from_host(&host).expect("service_from_host should succeed");
257        // We just verify it returns successfully; the service is ()
258        assert_eq!(*svc, ());
259    }
260
261    // --- Link trait ---
262
263    #[derive(Clone, Debug)]
264    struct TestLink {
265        id: Uuid,
266        source_id: Uuid,
267        target_id: Uuid,
268        link_type: String,
269        created_at: DateTime<Utc>,
270        updated_at: DateTime<Utc>,
271        deleted_at: Option<DateTime<Utc>>,
272        status: String,
273    }
274
275    impl Entity for TestLink {
276        type Service = ();
277
278        fn resource_name() -> &'static str {
279            "test_links"
280        }
281
282        fn resource_name_singular() -> &'static str {
283            "test_link"
284        }
285
286        fn service_from_host(
287            _host: &Arc<dyn std::any::Any + Send + Sync>,
288        ) -> Result<Arc<Self::Service>> {
289            Ok(Arc::new(()))
290        }
291
292        fn id(&self) -> Uuid {
293            self.id
294        }
295
296        fn entity_type(&self) -> &str {
297            "test_link"
298        }
299
300        fn created_at(&self) -> DateTime<Utc> {
301            self.created_at
302        }
303
304        fn updated_at(&self) -> DateTime<Utc> {
305            self.updated_at
306        }
307
308        fn deleted_at(&self) -> Option<DateTime<Utc>> {
309            self.deleted_at
310        }
311
312        fn status(&self) -> &str {
313            &self.status
314        }
315    }
316
317    impl Link for TestLink {
318        fn source_id(&self) -> Uuid {
319            self.source_id
320        }
321
322        fn target_id(&self) -> Uuid {
323            self.target_id
324        }
325
326        fn link_type(&self) -> &str {
327            &self.link_type
328        }
329    }
330
331    #[test]
332    fn test_link_accessors() {
333        let now = Utc::now();
334        let src = Uuid::new_v4();
335        let tgt = Uuid::new_v4();
336        let link = TestLink {
337            id: Uuid::new_v4(),
338            source_id: src,
339            target_id: tgt,
340            link_type: "ownership".to_string(),
341            created_at: now,
342            updated_at: now,
343            deleted_at: None,
344            status: "active".to_string(),
345        };
346        assert_eq!(link.source_id(), src);
347        assert_eq!(link.target_id(), tgt);
348        assert_eq!(link.link_type(), "ownership");
349    }
350
351    #[test]
352    fn test_link_is_deleted_and_is_active() {
353        let now = Utc::now();
354        let mut link = TestLink {
355            id: Uuid::new_v4(),
356            source_id: Uuid::new_v4(),
357            target_id: Uuid::new_v4(),
358            link_type: "ref".to_string(),
359            created_at: now,
360            updated_at: now,
361            deleted_at: None,
362            status: "active".to_string(),
363        };
364        assert!(!link.is_deleted());
365        assert!(link.is_active());
366
367        link.deleted_at = Some(now);
368        assert!(link.is_deleted());
369        assert!(!link.is_active());
370    }
371
372    #[test]
373    fn test_link_display_does_not_panic() {
374        let now = Utc::now();
375        let link = TestLink {
376            id: Uuid::new_v4(),
377            source_id: Uuid::new_v4(),
378            target_id: Uuid::new_v4(),
379            link_type: "ref".to_string(),
380            created_at: now,
381            updated_at: now,
382            deleted_at: None,
383            status: "active".to_string(),
384        };
385        // Calling display() should not panic
386        link.display();
387    }
388
389    #[test]
390    fn test_link_inactive_status() {
391        let now = Utc::now();
392        let link = TestLink {
393            id: Uuid::new_v4(),
394            source_id: Uuid::new_v4(),
395            target_id: Uuid::new_v4(),
396            link_type: "ref".to_string(),
397            created_at: now,
398            updated_at: now,
399            deleted_at: None,
400            status: "suspended".to_string(),
401        };
402        assert!(!link.is_active());
403        assert!(!link.is_deleted());
404    }
405}