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 fn into_values(self) -> MutationValues;
44
45 fn dirty_fields(&self) -> Option<BTreeSet<String>> {
49 None
50 }
51
52 fn is_marked_as_delete(&self) -> bool {
54 false
55 }
56
57 fn is_new(&self) -> bool {
59 false
60 }
61
62 fn mark_as_new(&mut self) {}
64
65 fn get_comment(&self) -> Option<String> {
67 None
68 }
69
70 fn set_comment(&mut self, _comment: String) {}
72
73 fn audit_as(self, comment: impl Into<String>) -> Audited<Self> {
76 Audited::new(self, comment)
77 }
78
79 fn original_values(&self) -> Option<EntitySnapshot> {
81 None
82 }
83
84 #[allow(unused_variables)]
87 fn on_loaded(&mut self, context: &dyn std::any::Any) {}
88
89 fn into_json(self) -> serde_json::Value {
90 let values: BTreeMap<String, Value> = self.into_values().into();
91 record_to_json_value(&values)
92 }
93}
94
95pub struct Audited<T: Entity> {
99 inner: T,
100 comment: String,
101}
102
103impl<T: Entity> Audited<T> {
104 pub fn new(entity: T, comment: impl Into<String>) -> Self {
106 let comment = comment.into();
107 assert!(
108 !comment.trim().is_empty(),
109 "audit comment must not be empty"
110 );
111 Self {
112 inner: entity,
113 comment,
114 }
115 }
116
117 pub fn entity(&self) -> &T {
119 &self.inner
120 }
121
122 pub fn entity_mut(&mut self) -> &mut T {
124 &mut self.inner
125 }
126
127 pub fn into_entity(self) -> T {
129 let mut entity = self.inner;
130 entity.set_comment(self.comment);
131 entity
132 }
133
134 pub fn get_comment(&self) -> &str {
136 &self.comment
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Default)]
141pub struct BaseEntityData {
142 pub id: u64,
143 pub version: i64,
144 pub dynamic: BTreeMap<String, Value>,
145}
146
147impl BaseEntityData {
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn with_id(mut self, id: u64) -> Self {
153 self.id = id;
154 self
155 }
156
157 pub fn with_version(mut self, version: i64) -> Self {
158 self.version = version;
159 self
160 }
161
162 pub fn with_dynamic(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
163 self.dynamic.insert(key.into(), value.into());
164 self
165 }
166
167 pub fn dynamic(&self, key: &str) -> Option<&Value> {
168 self.dynamic.get(key)
169 }
170
171 pub fn dynamic_i64(&self, key: &str) -> Option<i64> {
172 self.dynamic(key).and_then(Value::try_i64)
173 }
174
175 pub fn dynamic_u64(&self, key: &str) -> Option<u64> {
176 self.dynamic(key).and_then(Value::try_u64)
177 }
178
179 pub fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
180 self.dynamic(key).and_then(Value::try_decimal)
181 }
182
183 pub fn dynamic_f64(&self, key: &str) -> Option<f64> {
184 self.dynamic(key).and_then(Value::try_f64)
185 }
186
187 pub fn dynamic_text(&self, key: &str) -> Option<&str> {
188 self.dynamic(key).and_then(Value::try_text)
189 }
190
191 pub fn dynamic_bool(&self, key: &str) -> Option<bool> {
192 self.dynamic(key).and_then(Value::try_bool)
193 }
194
195 pub fn put_dynamic(
196 &mut self,
197 key: impl Into<String>,
198 value: impl Into<Value>,
199 ) -> Option<Value> {
200 self.dynamic.insert(key.into(), value.into())
201 }
202
203 pub fn remove_dynamic(&mut self, key: &str) -> Option<Value> {
204 self.dynamic.remove(key)
205 }
206
207 pub fn to_values_map(&self) -> BTreeMap<String, Value> {
208 let mut values = BTreeMap::new();
209 values.insert("id".to_owned(), Value::U64(self.id));
210 values.insert("version".to_owned(), Value::I64(self.version));
211 for (key, value) in &self.dynamic {
212 values.insert(key.clone(), value.clone());
213 }
214 values
215 }
216
217 pub fn from_values_map(values: &BTreeMap<String, Value>) -> Result<Self, EntityError> {
218 let id = match values.get("id") {
219 Some(Value::U64(v)) => *v,
220 Some(Value::I64(v)) if *v >= 0 => *v as u64,
221 Some(Value::Null) | None => 0,
222 other => {
223 return Err(EntityError::new(
224 "BaseEntity",
225 format!("invalid id field: {other:?}"),
226 ));
227 }
228 };
229
230 let version = match values.get("version") {
231 Some(Value::I64(v)) => *v,
232 Some(Value::Null) | None => 0,
233 other => {
234 return Err(EntityError::new(
235 "BaseEntity",
236 format!("invalid version field: {other:?}"),
237 ));
238 }
239 };
240
241 let dynamic = values
242 .iter()
243 .filter(|(key, _)| key.as_str() != "id" && key.as_str() != "version")
244 .map(|(key, value)| (key.clone(), value.clone()))
245 .collect();
246
247 Ok(Self {
248 id,
249 version,
250 dynamic,
251 })
252 }
253}
254
255pub trait BaseEntity: Entity {
256 fn base(&self) -> &BaseEntityData;
257 fn base_mut(&mut self) -> &mut BaseEntityData;
258
259 fn id(&self) -> u64 {
260 self.base().id
261 }
262
263 fn set_id(&mut self, id: u64) {
264 self.base_mut().id = id;
265 }
266
267 fn version_value(&self) -> i64 {
268 self.base().version
269 }
270
271 fn set_version(&mut self, version: i64) {
272 self.base_mut().version = version;
273 }
274
275 fn dynamic(&self, key: &str) -> Option<&Value> {
276 self.base().dynamic(key)
277 }
278
279 fn dynamic_i64(&self, key: &str) -> Option<i64> {
280 self.base().dynamic_i64(key)
281 }
282
283 fn dynamic_u64(&self, key: &str) -> Option<u64> {
284 self.base().dynamic_u64(key)
285 }
286
287 fn dynamic_decimal(&self, key: &str) -> Option<Decimal> {
288 self.base().dynamic_decimal(key)
289 }
290
291 fn dynamic_f64(&self, key: &str) -> Option<f64> {
292 self.base().dynamic_f64(key)
293 }
294
295 fn dynamic_text(&self, key: &str) -> Option<&str> {
296 self.base().dynamic_text(key)
297 }
298
299 fn dynamic_bool(&self, key: &str) -> Option<bool> {
300 self.base().dynamic_bool(key)
301 }
302
303 fn put_dynamic(&mut self, key: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
304 self.base_mut().put_dynamic(key, value)
305 }
306}
307
308pub trait IdentifiableEntity: Entity {
309 fn id_value(&self) -> Value;
310}
311
312pub trait VersionedEntity: Entity {
313 fn version(&self) -> i64;
314}
315
316pub trait TeaqlBoxedRelations: Sized {
317 fn extend_descriptor(descriptor: &mut EntityDescriptor);
318 fn extract_from_values(values: &CompactRow) -> Result<Self, EntityError>;
319 fn inject_into_values(self, values: &mut BTreeMap<String, Value>);
320}
321
322impl<T: TeaqlBoxedRelations> TeaqlBoxedRelations for Box<T> {
323 fn extend_descriptor(descriptor: &mut EntityDescriptor) {
324 T::extend_descriptor(descriptor);
325 }
326 fn extract_from_values(values: &CompactRow) -> Result<Self, EntityError> {
327 Ok(Box::new(T::extract_from_values(values)?))
328 }
329 fn inject_into_values(self, values: &mut BTreeMap<String, Value>) {
330 (*self).inject_into_values(values);
331 }
332}
333
334pub trait EntityDescriptorStore {
335 fn register_descriptor(&mut self, descriptor: EntityDescriptor);
336}
337
338#[macro_export]
339macro_rules! register_entities {
340 ($store:expr, $($entity:ty),+ $(,)?) => {{
341 $(
342 <$entity as $crate::TeaqlEntity>::register_into($store);
343 )+
344 }};
345}