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