Skip to main content

teaql_runtime/
checker.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use teaql_core::{Entity, Record, TeaqlEntity, Value};
5
6use crate::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_record(record: &Record) -> Self {
27        match record.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 record.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_record_status(record: &mut Record, status: CheckObjectStatus) {
53    record.insert(CHECK_OBJECT_STATUS_FIELD.to_owned(), status.into());
54}
55
56pub fn clear_record_status(record: &mut Record) {
57    record.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}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum LocationSegment {
71    Member(String),
72    Index(usize),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Default)]
76pub struct ObjectLocation {
77    segments: Vec<LocationSegment>,
78}
79
80impl ObjectLocation {
81    pub fn root() -> Self {
82        Self::default()
83    }
84
85    pub fn hash_root(member: impl Into<String>) -> Self {
86        Self::root().member(member)
87    }
88
89    pub fn array_root(index: usize) -> Self {
90        Self::root().element(index)
91    }
92
93    pub fn member(mut self, member: impl Into<String>) -> Self {
94        self.segments.push(LocationSegment::Member(member.into()));
95        self
96    }
97
98    pub fn element(mut self, index: usize) -> Self {
99        self.segments.push(LocationSegment::Index(index));
100        self
101    }
102
103    pub fn is_root(&self) -> bool {
104        self.segments.is_empty()
105    }
106
107    pub fn level(&self) -> usize {
108        self.segments.len()
109    }
110}
111
112impl std::fmt::Display for ObjectLocation {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        if self.segments.is_empty() {
115            return write!(f, "$");
116        }
117        let mut first = true;
118        for segment in &self.segments {
119            match segment {
120                LocationSegment::Member(member) => {
121                    if !first {
122                        write!(f, ".")?;
123                    }
124                    write!(f, "{member}")?;
125                }
126                LocationSegment::Index(index) => {
127                    write!(f, "[{index}]")?;
128                }
129            }
130            first = false;
131        }
132        Ok(())
133    }
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct CheckResult {
138    pub rule: CheckRule,
139    pub location: ObjectLocation,
140    pub input_value: Option<Value>,
141    pub system_value: Option<Value>,
142    pub message: Option<String>,
143}
144
145impl CheckResult {
146    pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
147        Self {
148            rule,
149            location,
150            input_value: None,
151            system_value: None,
152            message: None,
153        }
154    }
155
156    pub fn required(location: ObjectLocation) -> Self {
157        Self::new(CheckRule::Required, location)
158    }
159
160    pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
161        Self::new(CheckRule::Min, location)
162            .with_system_value(min)
163            .with_input_value(current)
164    }
165
166    pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
167        Self::new(CheckRule::Max, location)
168            .with_system_value(max)
169            .with_input_value(current)
170    }
171
172    pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
173        Self::new(CheckRule::MinStringLength, location)
174            .with_system_value(min_len)
175            .with_input_value(current)
176    }
177
178    pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
179        Self::new(CheckRule::MaxStringLength, location)
180            .with_system_value(max_len)
181            .with_input_value(current)
182    }
183
184    pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
185        self.input_value = Some(value.into());
186        self
187    }
188
189    pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
190        self.system_value = Some(value.into());
191        self
192    }
193
194    pub fn with_message(mut self, message: impl Into<String>) -> Self {
195        self.message = Some(message.into());
196        self
197    }
198}
199
200impl std::fmt::Display for CheckResult {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match &self.message {
203            Some(message) => write!(f, "{message}"),
204            None => write!(f, "{}: {:?}", self.location, self.rule),
205        }
206    }
207}
208
209pub type CheckResults = Vec<CheckResult>;
210
211pub trait Checker: Send + Sync {
212    fn entity(&self) -> &str;
213
214    fn check_and_fix(
215        &self,
216        ctx: &UserContext,
217        record: &mut Record,
218        location: &ObjectLocation,
219        results: &mut CheckResults,
220    );
221
222    fn required(
223        &self,
224        record: &Record,
225        field: &str,
226        location: &ObjectLocation,
227        results: &mut CheckResults,
228    ) {
229        if matches!(record.get(field), None | Some(Value::Null)) {
230            results.push(CheckResult::required(location.clone().member(field)));
231        }
232    }
233
234    fn min_string_length(
235        &self,
236        record: &Record,
237        field: &str,
238        min_len: usize,
239        location: &ObjectLocation,
240        results: &mut CheckResults,
241    ) {
242        if let Some(Value::Text(value)) = record.get(field) {
243            if value.chars().count() < min_len {
244                results.push(CheckResult::min_str(
245                    location.clone().member(field),
246                    min_len as u64,
247                    value.clone(),
248                ));
249            }
250        }
251    }
252
253    fn max_string_length(
254        &self,
255        record: &Record,
256        field: &str,
257        max_len: usize,
258        location: &ObjectLocation,
259        results: &mut CheckResults,
260    ) {
261        if let Some(Value::Text(value)) = record.get(field) {
262            if value.chars().count() > max_len {
263                results.push(CheckResult::max_str(
264                    location.clone().member(field),
265                    max_len as u64,
266                    value.clone(),
267                ));
268            }
269        }
270    }
271}
272
273pub trait CheckerRegistry: Send + Sync {
274    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
275}
276
277#[derive(Default, Clone)]
278pub struct InMemoryCheckerRegistry {
279    checkers: BTreeMap<String, Arc<dyn Checker>>,
280}
281
282impl InMemoryCheckerRegistry {
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    pub fn register(&mut self, checker: impl Checker + 'static) {
288        self.checkers
289            .insert(checker.entity().to_owned(), Arc::new(checker));
290    }
291
292    pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
293        self.register(checker);
294        self
295    }
296}
297
298impl CheckerRegistry for InMemoryCheckerRegistry {
299    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
300        self.checkers.get(entity).cloned()
301    }
302}
303
304// ---------------------------------------------------------------------------
305// TypedChecker & TypedEntityChecker
306// ---------------------------------------------------------------------------
307
308/// Typed version of [`Checker`] that works with concrete entity types (`T`)
309/// instead of generic [`Record`]s.
310///
311/// Implement this trait for per-entity checker logic structs, then wrap
312/// them in [`TypedEntityChecker`] so they satisfy the [`Checker`] trait
313/// expected by [`InMemoryCheckerRegistry`].
314pub trait TypedChecker<T>: Send + Sync {
315    fn check_and_fix_typed(
316        &self,
317        ctx: &UserContext,
318        entity: &mut T,
319        status: CheckObjectStatus,
320        location: &ObjectLocation,
321        results: &mut CheckResults,
322    );
323}
324
325/// Adapter that turns a [`TypedChecker<T>`] into a [`Checker`].
326///
327/// On [`Checker::check_and_fix`], it:
328/// 1. Extracts [`CheckObjectStatus`] from the `Record`.
329/// 2. Deserializes the `Record` into `T` via [`Entity::from_record`].
330/// 3. Delegates to [`TypedChecker::check_and_fix_typed`].
331/// 4. Serializes the (possibly mutated) `T` back into the `Record`
332///    via [`Entity::into_record`].
333pub struct TypedEntityChecker<T, C> {
334    checker: C,
335    entity_name: String,
336    _marker: std::marker::PhantomData<fn() -> T>,
337}
338
339impl<T, C> TypedEntityChecker<T, C>
340where
341    T: TeaqlEntity,
342{
343    /// Create a new `TypedEntityChecker` wrapping `checker`.
344    pub fn new(checker: C) -> Self {
345        let entity_name = T::entity_descriptor().name.clone();
346        Self {
347            checker,
348            entity_name,
349            _marker: std::marker::PhantomData,
350        }
351    }
352}
353
354impl<T, C> Checker for TypedEntityChecker<T, C>
355where
356    T: Entity + TeaqlEntity + Send + Sync + Clone,
357    C: TypedChecker<T>,
358{
359    fn entity(&self) -> &str {
360        &self.entity_name
361    }
362
363    fn check_and_fix(
364        &self,
365        ctx: &UserContext,
366        record: &mut Record,
367        location: &ObjectLocation,
368        results: &mut CheckResults,
369    ) {
370        let status = CheckObjectStatus::from_record(record);
371        // Take ownership of the record (replace with empty) so we can
372        // call T::from_record which consumes the Record.
373        let owned_record = std::mem::take(record);
374        match T::from_record(owned_record) {
375            Ok(mut entity) => {
376                self.checker
377                    .check_and_fix_typed(ctx, &mut entity, status, location, results);
378                // Write mutated entity back into the original record slot.
379                *record = entity.into_record();
380            }
381            Err(_e) => {
382                // If deserialization fails, re-build an empty record so
383                // the caller always sees a valid (though empty) Record.
384                *record = Record::default();
385                // Push a generic error result.
386                results.push(CheckResult::new(CheckRule::Required, location.clone()));
387            }
388        }
389    }
390}