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!(!comment.trim().is_empty(), "audit comment must not be empty");
102        Self { inner: entity, comment }
103    }
104
105    /// Access the inner entity by reference.
106    pub fn entity(&self) -> &T {
107        &self.inner
108    }
109
110    /// Access the inner entity by mutable reference.
111    pub fn entity_mut(&mut self) -> &mut T {
112        &mut self.inner
113    }
114
115    /// Consume and return the inner entity with comment applied.
116    pub fn into_entity(self) -> T {
117        let mut entity = self.inner;
118        entity.set_comment(self.comment);
119        entity
120    }
121
122    /// Get the comment.
123    pub fn get_comment(&self) -> &str {
124        &self.comment
125    }
126}
127
128#[derive(Debug, Clone, PartialEq, Default)]
129pub struct BaseEntityData {
130    pub id: u64,
131    pub version: i64,
132    pub dynamic: BTreeMap<String, Value>,
133}
134
135impl BaseEntityData {
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    pub fn with_id(mut self, id: u64) -> Self {
141        self.id = id;
142        self
143    }
144
145    pub fn with_version(mut self, version: i64) -> Self {
146        self.version = version;
147        self
148    }
149
150    pub fn with_dynamic(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
151        self.dynamic.insert(key.into(), value.into());
152        self
153    }
154
155    pub fn dynamic(&self, key: &str) -> Option<&Value> {
156        self.dynamic.get(key)
157    }
158
159    pub fn dynamic_i64(&self, key: &str) -> Option<i64> {
160        self.dynamic(key).and_then(Value::try_i64)
161    }
162
163    pub fn dynamic_u64(&self, key: &str) -> Option<u64> {
164        self.dynamic(key).and_then(Value::try_u64)
165    }
166
167    pub fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
168        self.dynamic(key).and_then(Value::try_decimal)
169    }
170
171    pub fn dynamic_f64(&self, key: &str) -> Option<f64> {
172        self.dynamic(key).and_then(Value::try_f64)
173    }
174
175    pub fn dynamic_text(&self, key: &str) -> Option<&str> {
176        self.dynamic(key).and_then(Value::try_text)
177    }
178
179    pub fn dynamic_bool(&self, key: &str) -> Option<bool> {
180        self.dynamic(key).and_then(Value::try_bool)
181    }
182
183    pub fn put_dynamic(
184        &mut self,
185        key: impl Into<String>,
186        value: impl Into<Value>,
187    ) -> Option<Value> {
188        self.dynamic.insert(key.into(), value.into())
189    }
190
191    pub fn remove_dynamic(&mut self, key: &str) -> Option<Value> {
192        self.dynamic.remove(key)
193    }
194
195    pub fn to_record(&self) -> Record {
196        let mut record = Record::new();
197        record.insert("id".to_owned(), Value::U64(self.id));
198        record.insert("version".to_owned(), Value::I64(self.version));
199        for (key, value) in &self.dynamic {
200            record.insert(key.clone(), value.clone());
201        }
202        record
203    }
204
205    pub fn from_record(record: &Record) -> Result<Self, EntityError> {
206        let id = match record.get("id") {
207            Some(Value::U64(v)) => *v,
208            Some(Value::I64(v)) if *v >= 0 => *v as u64,
209            Some(Value::Null) | None => 0,
210            other => {
211                return Err(EntityError::new(
212                    "BaseEntity",
213                    format!("invalid id field: {other:?}"),
214                ));
215            }
216        };
217
218        let version = match record.get("version") {
219            Some(Value::I64(v)) => *v,
220            Some(Value::Null) | None => 0,
221            other => {
222                return Err(EntityError::new(
223                    "BaseEntity",
224                    format!("invalid version field: {other:?}"),
225                ));
226            }
227        };
228
229        let dynamic = record
230            .iter()
231            .filter(|(key, _)| key.as_str() != "id" && key.as_str() != "version")
232            .map(|(key, value)| (key.clone(), value.clone()))
233            .collect();
234
235        Ok(Self {
236            id,
237            version,
238            dynamic,
239        })
240    }
241}
242
243pub trait BaseEntity: Entity {
244    fn base(&self) -> &BaseEntityData;
245    fn base_mut(&mut self) -> &mut BaseEntityData;
246
247    fn id(&self) -> u64 {
248        self.base().id
249    }
250
251    fn set_id(&mut self, id: u64) {
252        self.base_mut().id = id;
253    }
254
255    fn version_value(&self) -> i64 {
256        self.base().version
257    }
258
259    fn set_version(&mut self, version: i64) {
260        self.base_mut().version = version;
261    }
262
263    fn dynamic(&self, key: &str) -> Option<&Value> {
264        self.base().dynamic(key)
265    }
266
267    fn dynamic_i64(&self, key: &str) -> Option<i64> {
268        self.base().dynamic_i64(key)
269    }
270
271    fn dynamic_u64(&self, key: &str) -> Option<u64> {
272        self.base().dynamic_u64(key)
273    }
274
275    fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
276        self.base().dynamic_decimal(key)
277    }
278
279    fn dynamic_f64(&self, key: &str) -> Option<f64> {
280        self.base().dynamic_f64(key)
281    }
282
283    fn dynamic_text(&self, key: &str) -> Option<&str> {
284        self.base().dynamic_text(key)
285    }
286
287    fn dynamic_bool(&self, key: &str) -> Option<bool> {
288        self.base().dynamic_bool(key)
289    }
290
291    fn put_dynamic(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
292        self.base_mut().put_dynamic(key, value)
293    }
294}
295
296pub trait IdentifiableEntity: Entity {
297    fn id_value(&self) -> Value;
298}
299
300pub trait VersionedEntity: Entity {
301    fn version(&self) -> i64;
302}
303
304pub trait EntityDescriptorStore {
305    fn register_descriptor(&mut self, descriptor: EntityDescriptor);
306}
307
308#[macro_export]
309macro_rules! register_entities {
310    ($store:expr, $($entity:ty),+ $(,)?) => {{
311        $(
312            <$entity as $crate::TeaqlEntity>::register_into($store);
313        )+
314    }};
315}