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