Skip to main content

teaql_runtime/
entity_runtime.rs

1use std::collections::BTreeMap;
2use std::sync::{Arc, Mutex};
3
4use teaql_core::{Record, Value};
5
6#[derive(Debug, Clone)]
7pub struct EntityKey {
8    pub entity: String,
9    pub id: Value,
10    id_key: String,
11}
12
13impl EntityKey {
14    pub fn new(entity: impl Into<String>, id: impl Into<Value>) -> Self {
15        let id = id.into();
16        Self {
17            entity: entity.into(),
18            id_key: value_key(&id),
19            id,
20        }
21    }
22}
23
24impl PartialEq for EntityKey {
25    fn eq(&self, other: &Self) -> bool {
26        self.entity == other.entity && self.id_key == other.id_key
27    }
28}
29
30impl Eq for EntityKey {}
31
32impl PartialOrd for EntityKey {
33    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
34        Some(self.cmp(other))
35    }
36}
37
38impl Ord for EntityKey {
39    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40        self.entity
41            .cmp(&other.entity)
42            .then_with(|| self.id_key.cmp(&other.id_key))
43    }
44}
45
46fn value_key(value: &Value) -> String {
47    match value {
48        Value::Null => "null".to_owned(),
49        Value::Bool(value) => format!("bool:{value}"),
50        Value::I64(value) => format!("i64:{value}"),
51        Value::U64(value) => format!("u64:{value}"),
52        Value::F64(value) => format!("f64:{value}"),
53        Value::Decimal(value) => format!("decimal:{value}"),
54        Value::Text(value) => format!("text:{value}"),
55        Value::Json(value) => format!("json:{value}"),
56        Value::Date(value) => format!("date:{value}"),
57        Value::Timestamp(value) => format!("timestamp:{}", value.to_rfc3339()),
58        Value::Object(_) => "object".to_owned(),
59        Value::List(_) => "list".to_owned(),
60    }
61}
62
63#[derive(Debug, Clone, Default, PartialEq)]
64pub struct EntityChangeSet {
65    changes: BTreeMap<EntityKey, Record>,
66}
67
68impl EntityChangeSet {
69    pub fn is_empty(&self) -> bool {
70        self.changes.is_empty()
71    }
72
73    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
74        self.changes
75            .entry(key)
76            .or_default()
77            .insert(field.into(), value);
78    }
79
80    pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
81        self.changes.get(key).and_then(|changes| changes.get(field))
82    }
83
84    pub fn changes(&self) -> &BTreeMap<EntityKey, Record> {
85        &self.changes
86    }
87}
88
89#[derive(Debug, Clone, Default, PartialEq)]
90pub struct ChangeSetStack {
91    stack: Vec<EntityChangeSet>,
92}
93
94impl ChangeSetStack {
95    pub fn current_mut(&mut self) -> &mut EntityChangeSet {
96        if self.stack.is_empty() {
97            self.stack.push(EntityChangeSet::default());
98        }
99        self.stack.last_mut().expect("change set stack has current")
100    }
101
102    pub fn current(&self) -> Option<&EntityChangeSet> {
103        self.stack.last()
104    }
105
106    pub fn push(&mut self) {
107        self.stack.push(EntityChangeSet::default());
108    }
109
110    pub fn pop(&mut self) -> Option<EntityChangeSet> {
111        self.stack.pop()
112    }
113
114    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
115        self.stack
116            .iter()
117            .rev()
118            .find_map(|change_set| change_set.get(key, field).cloned())
119    }
120
121    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
122        self.current_mut().set(key, field, value);
123    }
124
125    pub fn clear_current(&mut self) {
126        if let Some(current) = self.stack.last_mut() {
127            *current = EntityChangeSet::default();
128        }
129    }
130}
131
132#[derive(Debug, Default)]
133pub struct RootContext {
134    change_sets: ChangeSetStack,
135    /// Annotation comment for observability during graph save.
136    comment: Option<String>,
137}
138
139#[derive(Debug, Clone, Default)]
140pub struct EntityRoot {
141    inner: Arc<Mutex<RootContext>>,
142}
143
144impl PartialEq for EntityRoot {
145    fn eq(&self, other: &Self) -> bool {
146        Arc::ptr_eq(&self.inner, &other.inner)
147    }
148}
149
150impl EntityRoot {
151    pub fn push_change_set(&self) {
152        self.inner
153            .lock()
154            .expect("entity root mutex")
155            .change_sets
156            .push();
157    }
158
159    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
160        self.inner
161            .lock()
162            .expect("entity root mutex")
163            .change_sets
164            .pop()
165    }
166
167    pub fn clear_current_change_set(&self) {
168        self.inner
169            .lock()
170            .expect("entity root mutex")
171            .change_sets
172            .clear_current();
173    }
174
175    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
176        self.inner
177            .lock()
178            .expect("entity root mutex")
179            .change_sets
180            .set(key, field, value.into());
181    }
182
183    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
184        self.inner
185            .lock()
186            .expect("entity root mutex")
187            .change_sets
188            .get(key, field)
189    }
190
191    pub fn current_change_set(&self) -> EntityChangeSet {
192        self.inner
193            .lock()
194            .expect("entity root mutex")
195            .change_sets
196            .current()
197            .cloned()
198            .unwrap_or_default()
199    }
200
201    /// Set an annotation comment on this entity root.
202    /// The comment propagates through the graph save process for observability.
203    pub fn set_comment(&self, comment: impl Into<String>) {
204        self.inner
205            .lock()
206            .expect("entity root mutex")
207            .comment = Some(comment.into());
208    }
209
210    /// Get the annotation comment, if any.
211    pub fn get_comment(&self) -> Option<String> {
212        self.inner
213            .lock()
214            .expect("entity root mutex")
215            .comment
216            .clone()
217    }
218}