Skip to main content

teaql_core/
entity.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{Decimal, EntityDescriptor, Record, Value, record_to_json_value};
4
5pub trait TeaqlEntity {
6    const ENTITY_NAME: &'static str;
7
8    fn entity_descriptor() -> EntityDescriptor;
9
10    fn register_into(store: &mut impl EntityDescriptorStore) {
11        store.register_descriptor(Self::entity_descriptor());
12    }
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct EntityError {
17    pub entity: String,
18    pub message: String,
19}
20
21impl EntityError {
22    pub fn new(entity: impl Into<String>, message: impl Into<String>) -> Self {
23        Self {
24            entity: entity.into(),
25            message: message.into(),
26        }
27    }
28}
29
30impl std::fmt::Display for EntityError {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "{}: {}", self.entity, self.message)
33    }
34}
35
36impl std::error::Error for EntityError {}
37
38pub trait Entity: TeaqlEntity + Sized {
39    fn from_record(record: Record) -> Result<Self, EntityError>;
40    fn into_record(self) -> Record;
41
42    /// Returns the set of field names that have been modified since the entity was loaded.
43    /// Returns `None` if dirty tracking is not available (backwards compatible default).
44    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
45    fn dirty_fields(&self) -> Option<BTreeSet<String>> {
46        None
47    }
48
49    /// Returns true if this entity has been marked for deletion.
50    fn is_marked_as_delete(&self) -> bool {
51        false
52    }
53
54    /// Returns true if this entity was explicitly constructed as a new entity.
55    fn is_new(&self) -> bool {
56        false
57    }
58
59    /// Mark this entity as a newly created entity, bypassing database existence checks.
60    fn mark_as_new(&mut self) {}
61
62    /// Get the annotation comment, if any.
63    fn get_comment(&self) -> Option<String> {
64        None
65    }
66
67    /// Set an annotation comment for this entity instance.
68    fn set_comment(&mut self, _comment: String) {}
69
70    /// Attach an audit comment and return a `Commented<Self>` wrapper.
71    /// This is the only way to unlock the `.save()` method.
72    fn audit_as(self, comment: impl Into<String>) -> Audited<Self> {
73        Audited::new(self, comment)
74    }
75
76    /// Get the original snapshot values when this entity was loaded from the repository, if available.
77    fn original_values(&self) -> Option<::std::collections::BTreeMap<String, Value>> {
78        None
79    }
80
81    /// Invoked immediately after the entity is loaded from the repository.
82    /// Used by implementations to attach runtime contexts or initialize internal states.
83    #[allow(unused_variables)]
84    fn on_loaded(&mut self, context: &dyn std::any::Any) {}
85
86    fn into_json(self) -> serde_json::Value {
87        record_to_json_value(&self.into_record())
88    }
89}
90
91/// A wrapper that carries a mandatory audit comment with an entity.
92/// Only `Commented<T>` has a `.save()` method — bare entities cannot be saved directly.
93/// This enforces the "must comment on save" policy at compile time.
94pub struct Audited<T: Entity> {
95    inner: T,
96    comment: String,
97}
98
99impl<T: Entity> Audited<T> {
100    /// Create a new Commented wrapper. Panics if comment is empty.
101    pub fn new(entity: T, comment: impl Into<String>) -> Self {
102        let comment = comment.into();
103        assert!(
104            !comment.trim().is_empty(),
105            "audit comment must not be empty"
106        );
107        Self {
108            inner: entity,
109            comment,
110        }
111    }
112
113    /// Access the inner entity by reference.
114    pub fn entity(&self) -> &T {
115        &self.inner
116    }
117
118    /// Access the inner entity by mutable reference.
119    pub fn entity_mut(&mut self) -> &mut T {
120        &mut self.inner
121    }
122
123    /// Consume and return the inner entity with comment applied.
124    pub fn into_entity(self) -> T {
125        let mut entity = self.inner;
126        entity.set_comment(self.comment);
127        entity
128    }
129
130    /// Get the comment.
131    pub fn get_comment(&self) -> &str {
132        &self.comment
133    }
134}
135
136#[derive(Debug, Clone, PartialEq, Default)]
137pub struct BaseEntityData {
138    pub id: u64,
139    pub version: i64,
140    pub dynamic: BTreeMap<String, Value>,
141}
142
143impl BaseEntityData {
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    pub fn with_id(mut self, id: u64) -> Self {
149        self.id = id;
150        self
151    }
152
153    pub fn with_version(mut self, version: i64) -> Self {
154        self.version = version;
155        self
156    }
157
158    pub fn with_dynamic(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
159        self.dynamic.insert(key.into(), value.into());
160        self
161    }
162
163    pub fn dynamic(&self, key: &str) -> Option<&Value> {
164        self.dynamic.get(key)
165    }
166
167    pub fn dynamic_i64(&self, key: &str) -> Option<i64> {
168        self.dynamic(key).and_then(Value::try_i64)
169    }
170
171    pub fn dynamic_u64(&self, key: &str) -> Option<u64> {
172        self.dynamic(key).and_then(Value::try_u64)
173    }
174
175    pub fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
176        self.dynamic(key).and_then(Value::try_decimal)
177    }
178
179    pub fn dynamic_f64(&self, key: &str) -> Option<f64> {
180        self.dynamic(key).and_then(Value::try_f64)
181    }
182
183    pub fn dynamic_text(&self, key: &str) -> Option<&str> {
184        self.dynamic(key).and_then(Value::try_text)
185    }
186
187    pub fn dynamic_bool(&self, key: &str) -> Option<bool> {
188        self.dynamic(key).and_then(Value::try_bool)
189    }
190
191    pub fn put_dynamic(
192        &mut self,
193        key: impl Into<String>,
194        value: impl Into<Value>,
195    ) -> Option<Value> {
196        self.dynamic.insert(key.into(), value.into())
197    }
198
199    pub fn remove_dynamic(&mut self, key: &str) -> Option<Value> {
200        self.dynamic.remove(key)
201    }
202
203    pub fn to_record(&self) -> Record {
204        let mut record = Record::new();
205        record.insert("id".to_owned(), Value::U64(self.id));
206        record.insert("version".to_owned(), Value::I64(self.version));
207        for (key, value) in &self.dynamic {
208            record.insert(key.clone(), value.clone());
209        }
210        record
211    }
212
213    pub fn from_record(record: &Record) -> Result<Self, EntityError> {
214        let id = match record.get("id") {
215            Some(Value::U64(v)) => *v,
216            Some(Value::I64(v)) if *v >= 0 => *v as u64,
217            Some(Value::Null) | None => 0,
218            other => {
219                return Err(EntityError::new(
220                    "BaseEntity",
221                    format!("invalid id field: {other:?}"),
222                ));
223            }
224        };
225
226        let version = match record.get("version") {
227            Some(Value::I64(v)) => *v,
228            Some(Value::Null) | None => 0,
229            other => {
230                return Err(EntityError::new(
231                    "BaseEntity",
232                    format!("invalid version field: {other:?}"),
233                ));
234            }
235        };
236
237        let dynamic = record
238            .iter()
239            .filter(|(key, _)| key.as_str() != "id" && key.as_str() != "version")
240            .map(|(key, value)| (key.clone(), value.clone()))
241            .collect();
242
243        Ok(Self {
244            id,
245            version,
246            dynamic,
247        })
248    }
249}
250
251pub trait BaseEntity: Entity {
252    fn base(&self) -> &BaseEntityData;
253    fn base_mut(&mut self) -> &mut BaseEntityData;
254
255    fn id(&self) -> u64 {
256        self.base().id
257    }
258
259    fn set_id(&mut self, id: u64) {
260        self.base_mut().id = id;
261    }
262
263    fn version_value(&self) -> i64 {
264        self.base().version
265    }
266
267    fn set_version(&mut self, version: i64) {
268        self.base_mut().version = version;
269    }
270
271    fn dynamic(&self, key: &str) -> Option<&Value> {
272        self.base().dynamic(key)
273    }
274
275    fn dynamic_i64(&self, key: &str) -> Option<i64> {
276        self.base().dynamic_i64(key)
277    }
278
279    fn dynamic_u64(&self, key: &str) -> Option<u64> {
280        self.base().dynamic_u64(key)
281    }
282
283    fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
284        self.base().dynamic_decimal(key)
285    }
286
287    fn dynamic_f64(&self, key: &str) -> Option<f64> {
288        self.base().dynamic_f64(key)
289    }
290
291    fn dynamic_text(&self, key: &str) -> Option<&str> {
292        self.base().dynamic_text(key)
293    }
294
295    fn dynamic_bool(&self, key: &str) -> Option<bool> {
296        self.base().dynamic_bool(key)
297    }
298
299    fn put_dynamic(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
300        self.base_mut().put_dynamic(key, value)
301    }
302}
303
304pub trait IdentifiableEntity: Entity {
305    fn id_value(&self) -> Value;
306}
307
308pub trait VersionedEntity: Entity {
309    fn version(&self) -> i64;
310}
311
312pub trait TeaqlBoxedRelations: Sized {
313    fn extend_descriptor(descriptor: &mut EntityDescriptor);
314    fn extract_from_record(record: &Record) -> Result<Self, EntityError>;
315    fn inject_into_record(self, record: &mut Record);
316}
317
318impl<T: TeaqlBoxedRelations> TeaqlBoxedRelations for Box<T> {
319    fn extend_descriptor(descriptor: &mut EntityDescriptor) {
320        T::extend_descriptor(descriptor);
321    }
322    fn extract_from_record(record: &Record) -> Result<Self, EntityError> {
323        Ok(Box::new(T::extract_from_record(record)?))
324    }
325    fn inject_into_record(self, record: &mut Record) {
326        (*self).inject_into_record(record);
327    }
328}
329
330pub trait EntityDescriptorStore {
331    fn register_descriptor(&mut self, descriptor: EntityDescriptor);
332}
333
334#[macro_export]
335macro_rules! register_entities {
336    ($store:expr, $($entity:ty),+ $(,)?) => {{
337        $(
338            <$entity as $crate::TeaqlEntity>::register_into($store);
339        )+
340    }};
341}