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