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 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    /// Get the original snapshot values when this entity was loaded from the repository, if available.
69    fn original_values(&self) -> Option<::std::collections::BTreeMap<String, Value>> {
70        None
71    }
72
73    /// Invoked immediately after the entity is loaded from the repository.
74    /// Used by implementations to attach runtime contexts or initialize internal states.
75    #[allow(unused_variables)]
76    fn on_loaded(&mut self, context: &dyn std::any::Any) {}
77
78    fn into_json(self) -> serde_json::Value {
79        record_to_json_value(&self.into_record())
80    }
81}
82
83#[derive(Debug, Clone, PartialEq, Default)]
84pub struct BaseEntityData {
85    pub id: u64,
86    pub version: i64,
87    pub dynamic: BTreeMap<String, Value>,
88}
89
90impl BaseEntityData {
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    pub fn with_id(mut self, id: u64) -> Self {
96        self.id = id;
97        self
98    }
99
100    pub fn with_version(mut self, version: i64) -> Self {
101        self.version = version;
102        self
103    }
104
105    pub fn with_dynamic(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
106        self.dynamic.insert(key.into(), value.into());
107        self
108    }
109
110    pub fn dynamic(&self, key: &str) -> Option<&Value> {
111        self.dynamic.get(key)
112    }
113
114    pub fn dynamic_i64(&self, key: &str) -> Option<i64> {
115        self.dynamic(key).and_then(Value::try_i64)
116    }
117
118    pub fn dynamic_u64(&self, key: &str) -> Option<u64> {
119        self.dynamic(key).and_then(Value::try_u64)
120    }
121
122    pub fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
123        self.dynamic(key).and_then(Value::try_decimal)
124    }
125
126    pub fn dynamic_f64(&self, key: &str) -> Option<f64> {
127        self.dynamic(key).and_then(Value::try_f64)
128    }
129
130    pub fn dynamic_text(&self, key: &str) -> Option<&str> {
131        self.dynamic(key).and_then(Value::try_text)
132    }
133
134    pub fn dynamic_bool(&self, key: &str) -> Option<bool> {
135        self.dynamic(key).and_then(Value::try_bool)
136    }
137
138    pub fn put_dynamic(
139        &mut self,
140        key: impl Into<String>,
141        value: impl Into<Value>,
142    ) -> Option<Value> {
143        self.dynamic.insert(key.into(), value.into())
144    }
145
146    pub fn remove_dynamic(&mut self, key: &str) -> Option<Value> {
147        self.dynamic.remove(key)
148    }
149
150    pub fn to_record(&self) -> Record {
151        let mut record = Record::new();
152        record.insert("id".to_owned(), Value::U64(self.id));
153        record.insert("version".to_owned(), Value::I64(self.version));
154        for (key, value) in &self.dynamic {
155            record.insert(key.clone(), value.clone());
156        }
157        record
158    }
159
160    pub fn from_record(record: &Record) -> Result<Self, EntityError> {
161        let id = match record.get("id") {
162            Some(Value::U64(v)) => *v,
163            Some(Value::I64(v)) if *v >= 0 => *v as u64,
164            Some(Value::Null) | None => 0,
165            other => {
166                return Err(EntityError::new(
167                    "BaseEntity",
168                    format!("invalid id field: {other:?}"),
169                ));
170            }
171        };
172
173        let version = match record.get("version") {
174            Some(Value::I64(v)) => *v,
175            Some(Value::Null) | None => 0,
176            other => {
177                return Err(EntityError::new(
178                    "BaseEntity",
179                    format!("invalid version field: {other:?}"),
180                ));
181            }
182        };
183
184        let dynamic = record
185            .iter()
186            .filter(|(key, _)| key.as_str() != "id" && key.as_str() != "version")
187            .map(|(key, value)| (key.clone(), value.clone()))
188            .collect();
189
190        Ok(Self {
191            id,
192            version,
193            dynamic,
194        })
195    }
196}
197
198pub trait BaseEntity: Entity {
199    fn base(&self) -> &BaseEntityData;
200    fn base_mut(&mut self) -> &mut BaseEntityData;
201
202    fn id(&self) -> u64 {
203        self.base().id
204    }
205
206    fn set_id(&mut self, id: u64) {
207        self.base_mut().id = id;
208    }
209
210    fn version_value(&self) -> i64 {
211        self.base().version
212    }
213
214    fn set_version(&mut self, version: i64) {
215        self.base_mut().version = version;
216    }
217
218    fn dynamic(&self, key: &str) -> Option<&Value> {
219        self.base().dynamic(key)
220    }
221
222    fn dynamic_i64(&self, key: &str) -> Option<i64> {
223        self.base().dynamic_i64(key)
224    }
225
226    fn dynamic_u64(&self, key: &str) -> Option<u64> {
227        self.base().dynamic_u64(key)
228    }
229
230    fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
231        self.base().dynamic_decimal(key)
232    }
233
234    fn dynamic_f64(&self, key: &str) -> Option<f64> {
235        self.base().dynamic_f64(key)
236    }
237
238    fn dynamic_text(&self, key: &str) -> Option<&str> {
239        self.base().dynamic_text(key)
240    }
241
242    fn dynamic_bool(&self, key: &str) -> Option<bool> {
243        self.base().dynamic_bool(key)
244    }
245
246    fn put_dynamic(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
247        self.base_mut().put_dynamic(key, value)
248    }
249}
250
251pub trait IdentifiableEntity: Entity {
252    fn id_value(&self) -> Value;
253}
254
255pub trait VersionedEntity: Entity {
256    fn version(&self) -> i64;
257}
258
259pub trait EntityDescriptorStore {
260    fn register_descriptor(&mut self, descriptor: EntityDescriptor);
261}
262
263#[macro_export]
264macro_rules! register_entities {
265    ($store:expr, $($entity:ty),+ $(,)?) => {{
266        $(
267            <$entity as $crate::TeaqlEntity>::register_into($store);
268        )+
269    }};
270}