Skip to main content

teaql_core/
entity.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    CompactRow, Decimal, EntityDescriptor, EntitySnapshot, MutationIntent, MutationValues, Value,
5    record_to_json_value,
6};
7
8/// A presentation-only, path-local guard. Never touches an entity's ledger.
9#[doc(hidden)]
10#[derive(Default)]
11pub struct EntityJsonTraversal {
12    path: Vec<(&'static str, usize)>,
13}
14
15impl EntityJsonTraversal {
16    pub fn enter(&mut self, entity: &'static str, address: usize) -> bool {
17        let expand = self.path.len() < 128 && !self.path.contains(&(entity, address));
18        self.path.push((entity, address));
19        expand
20    }
21
22    pub fn leave(&mut self) {
23        self.path.pop();
24    }
25}
26
27pub trait TeaqlEntity {
28    const ENTITY_NAME: &'static str;
29
30    fn entity_descriptor() -> EntityDescriptor;
31
32    /// Fixed metadata supplied by generation; hand-written adapters may remain unindexed.
33    fn field_layout() -> Result<Option<std::sync::Arc<crate::FieldLayout>>, EntityError> {
34        Ok(None)
35    }
36
37    fn register_into(store: &mut impl EntityDescriptorStore) {
38        store.register_descriptor(Self::entity_descriptor());
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct EntityError {
44    pub entity: String,
45    pub message: String,
46}
47
48impl EntityError {
49    pub fn new(entity: impl Into<String>, message: impl Into<String>) -> Self {
50        Self {
51            entity: entity.into(),
52            message: message.into(),
53        }
54    }
55}
56
57impl std::fmt::Display for EntityError {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}: {}", self.entity, self.message)
60    }
61}
62
63impl std::error::Error for EntityError {}
64
65pub trait Entity: TeaqlEntity + Sized {
66    /// Optional readonly schema: a known type does not make a missing property present.
67    fn dynamic_property_type(&self, key: &str) -> Option<crate::DataType> {
68        let state = self.loaded_state_snapshot()?;
69        state.dynamic_property_definitions()?.data_type(key)
70    }
71    /// Framework JSON hydration capability; readonly properties remain unnumbered data.
72    #[doc(hidden)]
73    fn supports_dynamic_property_load() -> bool {
74        false
75    }
76
77    /// Borrow a readonly derived property using its full `_`-prefixed name.
78    /// Missing properties and explicit null both return `None`; real zero,
79    /// false and empty text remain values. This never loads data or changes state.
80    fn dynamic_property(&self, _key: &str) -> Option<&Value> {
81        None
82    }
83
84    /// Presence is separate from the nullable derived-property read contract.
85    /// An explicit null is present; an absent key is not. Native fields and
86    /// persistent `#` extensions are not part of this namespace.
87    fn has_dynamic_property(&self, _key: &str) -> bool {
88        false
89    }
90
91    /// Runtime-owned persistent-extension carrier, separate from readonly dynamic properties.
92    fn dynamic_field_values(&self) -> Option<&crate::dynamic_fields::DynamicFieldValues> {
93        None
94    }
95
96    fn has_pending_dynamic_mutations(&self) -> bool {
97        false
98    }
99
100    /// Type-erased framework bridge without a core -> runtime dependency.
101    #[doc(hidden)]
102    fn __teaql_runtime_state_any(&self) -> Option<&dyn std::any::Any> {
103        None
104    }
105
106    fn update_dynamic_field(
107        &mut self,
108        code: &str,
109        _value: Value,
110    ) -> Result<(), crate::dynamic_fields::DynamicFieldError> {
111        Err(crate::dynamic_fields::DynamicFieldError {
112            code: "DYNAMIC_FIELD_CARRIER_MISSING",
113            field: code.to_owned(),
114        })
115    }
116
117    fn delete_dynamic_field(
118        &mut self,
119        code: &str,
120    ) -> Result<(), crate::dynamic_fields::DynamicFieldError> {
121        Err(crate::dynamic_fields::DynamicFieldError {
122            code: "DYNAMIC_FIELD_CARRIER_MISSING",
123            field: code.to_owned(),
124        })
125    }
126
127    fn supports_dynamic_field_load() -> bool {
128        false
129    }
130
131    fn loaded_state_snapshot(&self) -> Option<std::sync::Arc<crate::LoadedSnapshot>> {
132        None
133    }
134
135    /// Framework hydration only: it must not create mutation intent or share row payloads.
136    #[doc(hidden)]
137    fn install_loaded_dynamic_fields(
138        &mut self,
139        _values: crate::dynamic_fields::DynamicFieldValues,
140        _state: std::sync::Arc<crate::LoadedSnapshot>,
141    ) -> Result<(), EntityError> {
142        Err(EntityError::new(
143            Self::ENTITY_NAME,
144            "entity lacks the runtime-owned indexed dynamic-field carrier",
145        ))
146    }
147
148    fn from_compact_row(row: CompactRow) -> Result<Self, EntityError>;
149
150    fn from_compact_row_with_context(
151        row: CompactRow,
152        context: &dyn std::any::Any,
153    ) -> Result<Self, EntityError> {
154        let mut entity = Self::from_compact_row(row)?;
155        entity.on_loaded(context);
156        Ok(entity)
157    }
158
159    fn into_values(self) -> MutationValues;
160
161    /// Presentation bridge for macro-generated typed relation carriers.
162    /// Kept separate so JSON conversion cannot change the mutation contract.
163    #[doc(hidden)]
164    fn into_json_values(self) -> BTreeMap<String, Value> {
165        self.into_values().into()
166    }
167
168    /// Borrow an indexed graph view without cloning entities or triggering I/O.
169    /// Older/manual carriers retain their existing consuming presentation path.
170    #[doc(hidden)]
171    fn borrowed_json(&self, _traversal: &mut EntityJsonTraversal) -> Option<serde_json::Value> {
172        None
173    }
174
175    /// Whether a model field was loaded in the entity snapshot used for a
176    /// mutation. Implementations without projection tracking remain fully
177    /// loaded for backwards compatibility.
178    fn is_field_loaded(&self, _field: &str) -> bool {
179        true
180    }
181
182    /// Restore the load boundary while materializing the typed Checker view.
183    /// This is runtime metadata, not mutation intent.
184    fn set_checker_loaded_fields(&mut self, _fields: BTreeSet<String>) {}
185
186    /// Restore the caller's exact mutation boundary while materializing the
187    /// typed Checker view. This metadata is used only during validation and
188    /// must not widen the eventual database update.
189    fn set_checker_dirty_fields(&mut self, _fields: BTreeSet<String>, _values: &MutationValues) {}
190
191    /// Returns the set of field names that have been modified since the entity was loaded.
192    /// Returns `None` if dirty tracking is not available (backwards compatible default).
193    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
194    fn dirty_fields(&self) -> Option<BTreeSet<String>> {
195        None
196    }
197
198    /// Returns true if this entity has been marked for deletion.
199    fn is_marked_as_delete(&self) -> bool {
200        false
201    }
202
203    /// Returns true if this entity was explicitly constructed as a new entity.
204    fn is_new(&self) -> bool {
205        false
206    }
207
208    /// Mark this entity as a newly created entity, bypassing database existence checks.
209    fn mark_as_new(&mut self) {}
210
211    /// Get the annotation comment, if any.
212    fn get_comment(&self) -> Option<String> {
213        None
214    }
215
216    /// Set an annotation comment for this entity instance.
217    fn set_comment(&mut self, _comment: String) {}
218
219    /// Attach an audit comment and return a `Commented<Self>` wrapper.
220    /// This is the only way to unlock the `.save()` method.
221    fn audit_as(self, comment: impl Into<String>) -> Audited<Self> {
222        Audited::new(self, comment)
223    }
224
225    /// Get the original snapshot values when this entity was loaded from the repository, if available.
226    fn original_values(&self) -> Option<EntitySnapshot> {
227        None
228    }
229
230    /// Invoked immediately after the entity is loaded from the repository.
231    /// Used by implementations to attach runtime contexts or initialize internal states.
232    #[allow(unused_variables)]
233    fn on_loaded(&mut self, context: &dyn std::any::Any) {}
234
235    fn into_json(self) -> serde_json::Value {
236        if let Some(json) = self.borrowed_json(&mut EntityJsonTraversal::default()) {
237            return json;
238        }
239        // Persistent extensions are a separate carrier, never native mutation
240        // columns. Serialize only supplied values; absent definitions stay absent.
241        let extensions: serde_json::Map<String, serde_json::Value> = self
242            .dynamic_field_values()
243            .map(|fields| {
244                fields
245                    .values()
246                    .iter()
247                    .map(|(code, value)| (format!("#{code}"), value.to_json_value()))
248                    .collect()
249            })
250            .unwrap_or_default();
251        let mut values = self.into_json_values();
252        // These exact framework-owned keys belong to the mutation/checker
253        // contract, not to an external entity representation. Other `_` keys
254        // are legitimate readonly dynamic properties and must be preserved.
255        for key in [
256            "_comment",
257            "_dirty_fields",
258            "_original_values",
259            "_is_new",
260            "_is_deleted",
261        ] {
262            values.remove(key);
263        }
264        let mut json = record_to_json_value(&values);
265        json.as_object_mut()
266            .expect("entity serialization is an object")
267            .extend(extensions);
268        json
269    }
270}
271
272/// A wrapper that carries a mandatory audit comment with an entity.
273/// Only `Audited<T>` has a `.save()` method — bare entities cannot be saved directly.
274/// This enforces the "must comment on save" policy at compile time.
275pub struct Audited<T: Entity> {
276    inner: T,
277    comment: MutationIntent,
278}
279
280impl<T: Entity> Audited<T> {
281    /// Create an audited wrapper with a validated, request-owned root reason.
282    ///
283    /// The fluent constructor remains infallible in its type signature, but
284    /// invalid input panics with the same code, location and repair guidance as
285    /// `MutationIntent`. Callers handling untrusted input can validate it with
286    /// `MutationIntent::new` before constructing this wrapper.
287    pub fn new(entity: T, comment: impl Into<String>) -> Self {
288        let comment = MutationIntent::new(comment).unwrap_or_else(|error| panic!("{error}"));
289        Self {
290            inner: entity,
291            comment,
292        }
293    }
294
295    /// Access the inner entity by reference.
296    pub fn entity(&self) -> &T {
297        &self.inner
298    }
299
300    /// Access the inner entity by mutable reference.
301    pub fn entity_mut(&mut self) -> &mut T {
302        &mut self.inner
303    }
304
305    /// Consume and return the inner entity with comment applied.
306    pub fn into_entity(self) -> T {
307        let mut entity = self.inner;
308        entity.set_comment(self.comment.into_comment());
309        entity
310    }
311
312    /// Get the comment.
313    pub fn get_comment(&self) -> &str {
314        self.comment.comment()
315    }
316}
317
318#[derive(Debug, Clone, PartialEq, Default)]
319pub struct BaseEntityData {
320    pub id: u64,
321    pub version: i64,
322    pub dynamic: BTreeMap<String, Value>,
323}
324
325impl BaseEntityData {
326    pub fn new() -> Self {
327        Self::default()
328    }
329
330    pub fn with_id(mut self, id: u64) -> Self {
331        self.id = id;
332        self
333    }
334
335    pub fn with_version(mut self, version: i64) -> Self {
336        self.version = version;
337        self
338    }
339
340    pub fn with_dynamic(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
341        self.dynamic.insert(key.into(), value.into());
342        self
343    }
344
345    pub fn dynamic(&self, key: &str) -> Option<&Value> {
346        self.dynamic.get(key)
347    }
348
349    pub fn dynamic_i64(&self, key: &str) -> Option<i64> {
350        self.dynamic(key).and_then(Value::try_i64)
351    }
352
353    pub fn dynamic_u64(&self, key: &str) -> Option<u64> {
354        self.dynamic(key).and_then(Value::try_u64)
355    }
356
357    pub fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
358        self.dynamic(key).and_then(Value::try_decimal)
359    }
360
361    pub fn dynamic_f64(&self, key: &str) -> Option<f64> {
362        self.dynamic(key).and_then(Value::try_f64)
363    }
364
365    pub fn dynamic_text(&self, key: &str) -> Option<&str> {
366        self.dynamic(key).and_then(Value::try_text)
367    }
368
369    pub fn dynamic_bool(&self, key: &str) -> Option<bool> {
370        self.dynamic(key).and_then(Value::try_bool)
371    }
372
373    pub fn put_dynamic(
374        &mut self,
375        key: impl Into<String>,
376        value: impl Into<Value>,
377    ) -> Option<Value> {
378        self.dynamic.insert(key.into(), value.into())
379    }
380
381    pub fn remove_dynamic(&mut self, key: &str) -> Option<Value> {
382        self.dynamic.remove(key)
383    }
384
385    pub fn to_values_map(&self) -> BTreeMap<String, Value> {
386        let mut values = BTreeMap::new();
387        values.insert("id".to_owned(), Value::U64(self.id));
388        values.insert("version".to_owned(), Value::I64(self.version));
389        for (key, value) in &self.dynamic {
390            values.insert(key.clone(), value.clone());
391        }
392        values
393    }
394
395    pub fn from_values_map(values: &BTreeMap<String, Value>) -> Result<Self, EntityError> {
396        let id = match values.get("id") {
397            Some(Value::U64(v)) => *v,
398            Some(Value::I64(v)) if *v >= 0 => *v as u64,
399            Some(Value::Null) | None => 0,
400            other => {
401                return Err(EntityError::new(
402                    "BaseEntity",
403                    format!("invalid id field: {other:?}"),
404                ));
405            }
406        };
407
408        let version = match values.get("version") {
409            Some(Value::I64(v)) => *v,
410            Some(Value::Null) | None => 0,
411            other => {
412                return Err(EntityError::new(
413                    "BaseEntity",
414                    format!("invalid version field: {other:?}"),
415                ));
416            }
417        };
418
419        let dynamic = values
420            .iter()
421            .filter(|(key, _)| key.as_str() != "id" && key.as_str() != "version")
422            .map(|(key, value)| (key.clone(), value.clone()))
423            .collect();
424
425        Ok(Self {
426            id,
427            version,
428            dynamic,
429        })
430    }
431}
432
433pub trait BaseEntity: Entity {
434    fn base(&self) -> &BaseEntityData;
435    fn base_mut(&mut self) -> &mut BaseEntityData;
436
437    fn id(&self) -> u64 {
438        self.base().id
439    }
440
441    fn set_id(&mut self, id: u64) {
442        self.base_mut().id = id;
443    }
444
445    fn version_value(&self) -> i64 {
446        self.base().version
447    }
448
449    fn set_version(&mut self, version: i64) {
450        self.base_mut().version = version;
451    }
452
453    fn dynamic(&self, key: &str) -> Option<&Value> {
454        self.base().dynamic(key)
455    }
456
457    fn dynamic_i64(&self, key: &str) -> Option<i64> {
458        self.base().dynamic_i64(key)
459    }
460
461    fn dynamic_u64(&self, key: &str) -> Option<u64> {
462        self.base().dynamic_u64(key)
463    }
464
465    fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
466        self.base().dynamic_decimal(key)
467    }
468
469    fn dynamic_f64(&self, key: &str) -> Option<f64> {
470        self.base().dynamic_f64(key)
471    }
472
473    fn dynamic_text(&self, key: &str) -> Option<&str> {
474        self.base().dynamic_text(key)
475    }
476
477    fn dynamic_bool(&self, key: &str) -> Option<bool> {
478        self.base().dynamic_bool(key)
479    }
480
481    fn put_dynamic(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
482        self.base_mut().put_dynamic(key, value)
483    }
484}
485
486pub trait IdentifiableEntity: Entity {
487    fn id_value(&self) -> Value;
488}
489
490pub trait VersionedEntity: Entity {
491    fn version(&self) -> i64;
492}
493
494pub trait TeaqlBoxedRelations: Sized {
495    fn extend_descriptor(descriptor: &mut EntityDescriptor);
496    fn extract_from_values(values: &CompactRow) -> Result<Self, EntityError>;
497    fn inject_into_values(self, values: &mut BTreeMap<String, Value>);
498    #[doc(hidden)]
499    fn inject_into_json_values(
500        self,
501        values: &mut BTreeMap<String, Value>,
502        _loaded: Option<&crate::eval::LoadState>,
503    ) {
504        self.inject_into_values(values);
505    }
506}
507
508impl<T: TeaqlBoxedRelations> TeaqlBoxedRelations for Box<T> {
509    fn extend_descriptor(descriptor: &mut EntityDescriptor) {
510        T::extend_descriptor(descriptor);
511    }
512    fn extract_from_values(values: &CompactRow) -> Result<Self, EntityError> {
513        Ok(Box::new(T::extract_from_values(values)?))
514    }
515    fn inject_into_values(self, values: &mut BTreeMap<String, Value>) {
516        (*self).inject_into_values(values);
517    }
518    fn inject_into_json_values(
519        self,
520        values: &mut BTreeMap<String, Value>,
521        loaded: Option<&crate::eval::LoadState>,
522    ) {
523        (*self).inject_into_json_values(values, loaded);
524    }
525}
526
527pub trait EntityDescriptorStore {
528    fn register_descriptor(&mut self, descriptor: EntityDescriptor);
529}
530
531#[macro_export]
532macro_rules! register_entities {
533    ($store:expr, $($entity:ty),+ $(,)?) => {{
534        $(
535            <$entity as $crate::TeaqlEntity>::register_into($store);
536        )+
537    }};
538}