Skip to main content

teaql_runtime/
checker.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use teaql_core::{Entity, TeaqlEntity, Value};
5
6use crate::{EntityValues, UserContext};
7
8pub const CHECK_OBJECT_STATUS_FIELD: &str = "__teaql_object_status";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CheckObjectStatus {
12    Create,
13    Update,
14    Unknown,
15}
16
17impl CheckObjectStatus {
18    pub fn as_str(self) -> &'static str {
19        match self {
20            Self::Create => "create",
21            Self::Update => "update",
22            Self::Unknown => "unknown",
23        }
24    }
25
26    pub fn from_values(values: &EntityValues) -> Self {
27        match values.get(CHECK_OBJECT_STATUS_FIELD) {
28            Some(Value::Text(value)) if value == Self::Create.as_str() => Self::Create,
29            Some(Value::Text(value)) if value == Self::Update.as_str() => Self::Update,
30            _ => match values.get("id") {
31                None | Some(Value::Null) => Self::Create,
32                Some(_) => Self::Update,
33            },
34        }
35    }
36
37    pub fn is_create(self) -> bool {
38        matches!(self, Self::Create)
39    }
40
41    pub fn is_update(self) -> bool {
42        matches!(self, Self::Update)
43    }
44}
45
46impl From<CheckObjectStatus> for Value {
47    fn from(value: CheckObjectStatus) -> Self {
48        Value::Text(value.as_str().to_owned())
49    }
50}
51
52pub fn mark_entity_status(values: &mut EntityValues, status: CheckObjectStatus) {
53    values.insert(CHECK_OBJECT_STATUS_FIELD.to_owned(), status.into());
54}
55
56pub fn clear_entity_status(values: &mut EntityValues) {
57    values.remove(CHECK_OBJECT_STATUS_FIELD);
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum CheckRule {
62    Required,
63    Min,
64    Max,
65    MinStringLength,
66    MaxStringLength,
67    ContextRootMissing,
68    ContextRootMismatch,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum LocationSegment {
73    Member(String),
74    Index(usize),
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Default)]
78pub struct ObjectLocation {
79    segments: Vec<LocationSegment>,
80}
81
82impl ObjectLocation {
83    pub fn root() -> Self {
84        Self::default()
85    }
86
87    pub fn hash_root(member: impl Into<String>) -> Self {
88        Self::root().member(member)
89    }
90
91    pub fn array_root(index: usize) -> Self {
92        Self::root().element(index)
93    }
94
95    pub fn member(mut self, member: impl Into<String>) -> Self {
96        self.segments.push(LocationSegment::Member(member.into()));
97        self
98    }
99
100    pub fn element(mut self, index: usize) -> Self {
101        self.segments.push(LocationSegment::Index(index));
102        self
103    }
104
105    pub fn is_root(&self) -> bool {
106        self.segments.is_empty()
107    }
108
109    pub fn level(&self) -> usize {
110        self.segments.len()
111    }
112}
113
114impl std::fmt::Display for ObjectLocation {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        if self.segments.is_empty() {
117            return write!(f, "$");
118        }
119        let mut first = true;
120        for segment in &self.segments {
121            match segment {
122                LocationSegment::Member(member) => {
123                    if !first {
124                        write!(f, ".")?;
125                    }
126                    write!(f, "{member}")?;
127                }
128                LocationSegment::Index(index) => {
129                    write!(f, "[{index}]")?;
130                }
131            }
132            first = false;
133        }
134        Ok(())
135    }
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub struct CheckResult {
140    pub rule: CheckRule,
141    pub location: ObjectLocation,
142    pub input_value: Option<Value>,
143    pub system_value: Option<Value>,
144    pub message: Option<String>,
145}
146
147impl CheckResult {
148    pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
149        Self {
150            rule,
151            location,
152            input_value: None,
153            system_value: None,
154            message: None,
155        }
156    }
157
158    pub fn required(location: ObjectLocation) -> Self {
159        Self::new(CheckRule::Required, location)
160    }
161
162    pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
163        Self::new(CheckRule::Min, location)
164            .with_system_value(min)
165            .with_input_value(current)
166    }
167
168    pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
169        Self::new(CheckRule::Max, location)
170            .with_system_value(max)
171            .with_input_value(current)
172    }
173
174    pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
175        Self::new(CheckRule::MinStringLength, location)
176            .with_system_value(min_len)
177            .with_input_value(current)
178    }
179
180    pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
181        Self::new(CheckRule::MaxStringLength, location)
182            .with_system_value(max_len)
183            .with_input_value(current)
184    }
185
186    pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
187        self.input_value = Some(value.into());
188        self
189    }
190
191    pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
192        self.system_value = Some(value.into());
193        self
194    }
195
196    pub fn with_message(mut self, message: impl Into<String>) -> Self {
197        self.message = Some(message.into());
198        self
199    }
200}
201
202impl std::fmt::Display for CheckResult {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        match &self.message {
205            Some(message) => write!(f, "{message}"),
206            None => write!(f, "{}: {:?}", self.location, self.rule),
207        }
208    }
209}
210
211pub type CheckResults = Vec<CheckResult>;
212
213pub trait Checker: Send + Sync {
214    fn entity(&self) -> &str;
215
216    fn check_and_fix(
217        &self,
218        context: &UserContext,
219        values: &mut EntityValues,
220        location: &ObjectLocation,
221        results: &mut CheckResults,
222    );
223
224    fn required(
225        &self,
226        values: &EntityValues,
227        field: &str,
228        location: &ObjectLocation,
229        results: &mut CheckResults,
230    ) {
231        if matches!(values.get(field), None | Some(Value::Null)) {
232            results.push(CheckResult::required(location.clone().member(field)));
233        }
234    }
235
236    fn min_string_length(
237        &self,
238        values: &EntityValues,
239        field: &str,
240        min_len: usize,
241        location: &ObjectLocation,
242        results: &mut CheckResults,
243    ) {
244        if let Some(Value::Text(value)) = values.get(field) {
245            if value.chars().count() < min_len {
246                results.push(CheckResult::min_str(
247                    location.clone().member(field),
248                    min_len as u64,
249                    value.clone(),
250                ));
251            }
252        }
253    }
254
255    fn max_string_length(
256        &self,
257        values: &EntityValues,
258        field: &str,
259        max_len: usize,
260        location: &ObjectLocation,
261        results: &mut CheckResults,
262    ) {
263        if let Some(Value::Text(value)) = values.get(field) {
264            if value.chars().count() > max_len {
265                results.push(CheckResult::max_str(
266                    location.clone().member(field),
267                    max_len as u64,
268                    value.clone(),
269                ));
270            }
271        }
272    }
273}
274
275pub trait CheckerRegistry: Send + Sync {
276    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
277}
278
279#[derive(Default, Clone)]
280pub struct InMemoryCheckerRegistry {
281    checkers: BTreeMap<String, Arc<dyn Checker>>,
282}
283
284impl InMemoryCheckerRegistry {
285    pub fn new() -> Self {
286        Self::default()
287    }
288
289    pub fn register(&mut self, checker: impl Checker + 'static) {
290        self.checkers
291            .insert(checker.entity().to_owned(), Arc::new(checker));
292    }
293
294    pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
295        self.register(checker);
296        self
297    }
298}
299
300impl CheckerRegistry for InMemoryCheckerRegistry {
301    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
302        self.checkers.get(entity).cloned()
303    }
304}
305
306// ---------------------------------------------------------------------------
307// TypedChecker & TypedEntityChecker
308// ---------------------------------------------------------------------------
309
310/// Typed version of [`Checker`] that works with concrete entity types (`T`)
311/// instead of generic value maps.
312///
313/// Implement this trait for per-entity checker logic structs, then wrap
314/// them in [`TypedEntityChecker`] so they satisfy the [`Checker`] trait
315/// expected by [`InMemoryCheckerRegistry`].
316pub trait TypedChecker<T>: Send + Sync {
317    fn check_and_fix_typed(
318        &self,
319        context: &UserContext,
320        entity: &mut T,
321        status: CheckObjectStatus,
322        location: &ObjectLocation,
323        results: &mut CheckResults,
324    );
325}
326
327/// Adapter that turns a [`TypedChecker<T>`] into a [`Checker`].
328///
329/// On [`Checker::check_and_fix`], it:
330/// 1. Extracts [`CheckObjectStatus`] from the entity values.
331/// 2. Materializes `T` from a compact row.
332/// 3. Delegates to [`TypedChecker::check_and_fix_typed`].
333/// 4. Serializes the (possibly mutated) `T` back into entity values.
334pub struct TypedEntityChecker<T, C> {
335    checker: C,
336    entity_name: String,
337    _marker: std::marker::PhantomData<fn() -> T>,
338}
339
340impl<T, C> TypedEntityChecker<T, C>
341where
342    T: TeaqlEntity,
343{
344    /// Create a new `TypedEntityChecker` wrapping `checker`.
345    pub fn new(checker: C) -> Self {
346        let entity_name = T::entity_descriptor().name.clone();
347        Self {
348            checker,
349            entity_name,
350            _marker: std::marker::PhantomData,
351        }
352    }
353}
354
355impl<T, C> Checker for TypedEntityChecker<T, C>
356where
357    T: Entity + TeaqlEntity + Send + Sync + Clone,
358    C: TypedChecker<T>,
359{
360    fn entity(&self) -> &str {
361        &self.entity_name
362    }
363
364    fn check_and_fix(
365        &self,
366        context: &UserContext,
367        values: &mut EntityValues,
368        location: &ObjectLocation,
369        results: &mut CheckResults,
370    ) {
371        let status = CheckObjectStatus::from_values(values);
372        // Materializing a partial update necessarily fills omitted Rust fields
373        // with their type defaults. Those defaults are only a checker view;
374        // they must never become mutation intent. Keep the original sparse
375        // record and merge back only fields the typed checker actually changed.
376        let original_values = std::mem::take(values);
377        let owned_record = original_values.clone().into();
378        match T::from_compact_row(teaql_core::CompactRow::from_map(owned_record)) {
379            Ok(mut entity) => {
380                let before_check = entity.clone().into_values();
381                self.checker
382                    .check_and_fix_typed(context, &mut entity, status, location, results);
383                let after_check = entity.into_values();
384                *values = original_values;
385                for (field, after_value) in after_check {
386                    if before_check.get(&field) != Some(&after_value) {
387                        values.insert(field, after_value);
388                    }
389                }
390            }
391            Err(_e) => {
392                // If deserialization fails, re-build an empty record so
393                // the caller always sees a valid (though empty) entity value set.
394                *values = EntityValues::default();
395                // Push a generic error result.
396                results.push(CheckResult::new(CheckRule::Required, location.clone()));
397            }
398        }
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn test_object_location_formatting_and_nesting_levels() {
408        // Test root
409        let root = ObjectLocation::root();
410        assert_eq!(root.to_string(), "$");
411        assert!(root.is_root());
412        assert_eq!(root.level(), 0);
413
414        // Test hash_root
415        let hash = ObjectLocation::hash_root("user");
416        assert_eq!(hash.to_string(), "user");
417        assert!(!hash.is_root());
418        assert_eq!(hash.level(), 1);
419
420        // Test array_root
421        let arr = ObjectLocation::array_root(5);
422        assert_eq!(arr.to_string(), "[5]");
423        assert!(!arr.is_root());
424        assert_eq!(arr.level(), 1);
425
426        // Test nesting
427        let nested = ObjectLocation::root()
428            .member("users")
429            .element(2)
430            .member("address")
431            .member("city");
432
433        assert_eq!(nested.to_string(), "users[2].address.city");
434        assert_eq!(nested.level(), 4);
435    }
436
437    #[test]
438    fn test_check_object_status_inference_and_explicit_markers() {
439        let mut values = EntityValues::default();
440
441        // No id -> Create
442        assert_eq!(
443            CheckObjectStatus::from_values(&values),
444            CheckObjectStatus::Create
445        );
446
447        // Has id -> Update
448        values.insert("id".to_string(), Value::I64(1));
449        assert_eq!(
450            CheckObjectStatus::from_values(&values),
451            CheckObjectStatus::Update
452        );
453
454        // Explicit marker Create overrides id
455        mark_entity_status(&mut values, CheckObjectStatus::Create);
456        assert_eq!(
457            CheckObjectStatus::from_values(&values),
458            CheckObjectStatus::Create
459        );
460
461        // Explicit marker Update
462        mark_entity_status(&mut values, CheckObjectStatus::Update);
463        assert_eq!(
464            CheckObjectStatus::from_values(&values),
465            CheckObjectStatus::Update
466        );
467
468        // Clear marker
469        clear_entity_status(&mut values);
470        assert_eq!(
471            CheckObjectStatus::from_values(&values),
472            CheckObjectStatus::Update
473        ); // falls back to id -> Update
474    }
475}