Skip to main content

teaql_core/
entity.rs

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