Skip to main content

teaql_runtime/
checker.rs

1use std::collections::BTreeMap;
2use std::marker::PhantomData;
3use std::sync::Arc;
4
5use teaql_core::{Entity, Record, Value};
6
7use crate::UserContext;
8
9pub const CHECK_OBJECT_STATUS_FIELD: &str = "__teaql_object_status";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CheckObjectStatus {
13    Create,
14    Update,
15    Unknown,
16}
17
18impl CheckObjectStatus {
19    pub fn as_str(self) -> &'static str {
20        match self {
21            Self::Create => "create",
22            Self::Update => "update",
23            Self::Unknown => "unknown",
24        }
25    }
26
27    pub fn from_record(record: &Record) -> Self {
28        match record.get(CHECK_OBJECT_STATUS_FIELD) {
29            Some(Value::Text(value)) if value == Self::Create.as_str() => Self::Create,
30            Some(Value::Text(value)) if value == Self::Update.as_str() => Self::Update,
31            _ => match record.get("id") {
32                None | Some(Value::Null) => Self::Create,
33                Some(_) => Self::Update,
34            },
35        }
36    }
37
38    pub fn is_create(self) -> bool {
39        matches!(self, Self::Create)
40    }
41
42    pub fn is_update(self) -> bool {
43        matches!(self, Self::Update)
44    }
45}
46
47impl From<CheckObjectStatus> for Value {
48    fn from(value: CheckObjectStatus) -> Self {
49        Value::Text(value.as_str().to_owned())
50    }
51}
52
53pub fn mark_record_status(record: &mut Record, status: CheckObjectStatus) {
54    record.insert(CHECK_OBJECT_STATUS_FIELD.to_owned(), status.into());
55}
56
57pub fn clear_record_status(record: &mut Record) {
58    record.remove(CHECK_OBJECT_STATUS_FIELD);
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum CheckRule {
63    Required,
64    Min,
65    Max,
66    MinStringLength,
67    MaxStringLength,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum LocationSegment {
72    Member(String),
73    Index(usize),
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Default)]
77pub struct ObjectLocation {
78    segments: Vec<LocationSegment>,
79}
80
81impl ObjectLocation {
82    pub fn root() -> Self {
83        Self::default()
84    }
85
86    pub fn hash_root(member: impl Into<String>) -> Self {
87        Self::root().member(member)
88    }
89
90    pub fn array_root(index: usize) -> Self {
91        Self::root().element(index)
92    }
93
94    pub fn member(mut self, member: impl Into<String>) -> Self {
95        self.segments.push(LocationSegment::Member(member.into()));
96        self
97    }
98
99    pub fn element(mut self, index: usize) -> Self {
100        self.segments.push(LocationSegment::Index(index));
101        self
102    }
103
104    pub fn is_root(&self) -> bool {
105        self.segments.is_empty()
106    }
107
108    pub fn level(&self) -> usize {
109        self.segments.len()
110    }
111}
112
113impl std::fmt::Display for ObjectLocation {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        if self.segments.is_empty() {
116            return write!(f, "$");
117        }
118        let mut first = true;
119        for segment in &self.segments {
120            match segment {
121                LocationSegment::Member(member) => {
122                    if !first {
123                        write!(f, ".")?;
124                    }
125                    write!(f, "{member}")?;
126                }
127                LocationSegment::Index(index) => {
128                    write!(f, "[{index}]")?;
129                }
130            }
131            first = false;
132        }
133        Ok(())
134    }
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct CheckResult {
139    pub rule: CheckRule,
140    pub location: ObjectLocation,
141    pub input_value: Option<Value>,
142    pub system_value: Option<Value>,
143    pub message: Option<String>,
144}
145
146impl CheckResult {
147    pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
148        Self {
149            rule,
150            location,
151            input_value: None,
152            system_value: None,
153            message: None,
154        }
155    }
156
157    pub fn required(location: ObjectLocation) -> Self {
158        Self::new(CheckRule::Required, location)
159    }
160
161    pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
162        Self::new(CheckRule::Min, location)
163            .with_system_value(min)
164            .with_input_value(current)
165    }
166
167    pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
168        Self::new(CheckRule::Max, location)
169            .with_system_value(max)
170            .with_input_value(current)
171    }
172
173    pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
174        Self::new(CheckRule::MinStringLength, location)
175            .with_system_value(min_len)
176            .with_input_value(current)
177    }
178
179    pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
180        Self::new(CheckRule::MaxStringLength, location)
181            .with_system_value(max_len)
182            .with_input_value(current)
183    }
184
185    pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
186        self.input_value = Some(value.into());
187        self
188    }
189
190    pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
191        self.system_value = Some(value.into());
192        self
193    }
194
195    pub fn with_message(mut self, message: impl Into<String>) -> Self {
196        self.message = Some(message.into());
197        self
198    }
199}
200
201impl std::fmt::Display for CheckResult {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match &self.message {
204            Some(message) => write!(f, "{message}"),
205            None => write!(f, "{}: {:?}", self.location, self.rule),
206        }
207    }
208}
209
210pub type CheckResults = Vec<CheckResult>;
211
212pub trait Checker: Send + Sync {
213    fn entity(&self) -> &str;
214
215    fn check_and_fix(
216        &self,
217        ctx: &UserContext,
218        record: &mut Record,
219        location: &ObjectLocation,
220        results: &mut CheckResults,
221    );
222
223    fn required(
224        &self,
225        record: &Record,
226        field: &str,
227        location: &ObjectLocation,
228        results: &mut CheckResults,
229    ) {
230        if matches!(record.get(field), None | Some(Value::Null)) {
231            results.push(CheckResult::required(location.clone().member(field)));
232        }
233    }
234
235    fn min_string_length(
236        &self,
237        record: &Record,
238        field: &str,
239        min_len: usize,
240        location: &ObjectLocation,
241        results: &mut CheckResults,
242    ) {
243        if let Some(Value::Text(value)) = record.get(field) {
244            if value.chars().count() < min_len {
245                results.push(CheckResult::min_str(
246                    location.clone().member(field),
247                    min_len as u64,
248                    value.clone(),
249                ));
250            }
251        }
252    }
253
254    fn max_string_length(
255        &self,
256        record: &Record,
257        field: &str,
258        max_len: usize,
259        location: &ObjectLocation,
260        results: &mut CheckResults,
261    ) {
262        if let Some(Value::Text(value)) = record.get(field) {
263            if value.chars().count() > max_len {
264                results.push(CheckResult::max_str(
265                    location.clone().member(field),
266                    max_len as u64,
267                    value.clone(),
268                ));
269            }
270        }
271    }
272}
273
274pub trait TypedChecker<T>: Send + Sync
275where
276    T: Entity,
277{
278    fn check_and_fix_typed(
279        &self,
280        ctx: &UserContext,
281        entity: &mut T,
282        status: CheckObjectStatus,
283        location: &ObjectLocation,
284        results: &mut CheckResults,
285    );
286
287    fn required_option<V>(
288        &self,
289        value: Option<&V>,
290        field: &str,
291        location: &ObjectLocation,
292        results: &mut CheckResults,
293    ) {
294        if value.is_none() {
295            results.push(CheckResult::required(location.clone().member(field)));
296        }
297    }
298
299    fn required_text(
300        &self,
301        value: &str,
302        field: &str,
303        location: &ObjectLocation,
304        results: &mut CheckResults,
305    ) {
306        if value.trim().is_empty() {
307            results.push(CheckResult::required(location.clone().member(field)));
308        }
309    }
310
311    fn min_string_length(
312        &self,
313        value: &str,
314        field: &str,
315        min_len: usize,
316        location: &ObjectLocation,
317        results: &mut CheckResults,
318    ) {
319        if value.chars().count() < min_len {
320            results.push(CheckResult::min_str(
321                location.clone().member(field),
322                min_len as u64,
323                value.to_owned(),
324            ));
325        }
326    }
327
328    fn max_string_length(
329        &self,
330        value: &str,
331        field: &str,
332        max_len: usize,
333        location: &ObjectLocation,
334        results: &mut CheckResults,
335    ) {
336        if value.chars().count() > max_len {
337            results.push(CheckResult::max_str(
338                location.clone().member(field),
339                max_len as u64,
340                value.to_owned(),
341            ));
342        }
343    }
344}
345
346#[derive(Clone, Debug)]
347pub struct TypedEntityChecker<T, C> {
348    entity_name: String,
349    checker: C,
350    marker: PhantomData<T>,
351}
352
353impl<T, C> TypedEntityChecker<T, C>
354where
355    T: Entity,
356{
357    pub fn new(checker: C) -> Self {
358        Self {
359            entity_name: T::entity_descriptor().name,
360            checker,
361            marker: PhantomData,
362        }
363    }
364}
365
366impl<T, C> Checker for TypedEntityChecker<T, C>
367where
368    T: Entity + Send + Sync,
369    C: TypedChecker<T>,
370{
371    fn entity(&self) -> &str {
372        &self.entity_name
373    }
374
375    fn check_and_fix(
376        &self,
377        ctx: &UserContext,
378        record: &mut Record,
379        location: &ObjectLocation,
380        results: &mut CheckResults,
381    ) {
382        let status = CheckObjectStatus::from_record(record);
383        let status_value = record.get(CHECK_OBJECT_STATUS_FIELD).cloned();
384        let mut entity = match T::from_record(record.clone()) {
385            Ok(entity) => entity,
386            Err(err) => {
387                results.push(CheckResult::required(location.clone()).with_message(err.to_string()));
388                return;
389            }
390        };
391        self.checker
392            .check_and_fix_typed(ctx, &mut entity, status, location, results);
393        *record = entity.into_record();
394        if let Some(status_value) = status_value {
395            record.insert(CHECK_OBJECT_STATUS_FIELD.to_owned(), status_value);
396        }
397    }
398}
399
400pub trait CheckerRegistry: Send + Sync {
401    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
402}
403
404#[derive(Default, Clone)]
405pub struct InMemoryCheckerRegistry {
406    checkers: BTreeMap<String, Arc<dyn Checker>>,
407}
408
409impl InMemoryCheckerRegistry {
410    pub fn new() -> Self {
411        Self::default()
412    }
413
414    pub fn register(&mut self, checker: impl Checker + 'static) {
415        self.checkers
416            .insert(checker.entity().to_owned(), Arc::new(checker));
417    }
418
419    pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
420        self.register(checker);
421        self
422    }
423}
424
425impl CheckerRegistry for InMemoryCheckerRegistry {
426    fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
427        self.checkers.get(entity).cloned()
428    }
429}