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