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 InvalidType,
64 Min,
65 Max,
66 MinStringLength,
67 MaxStringLength,
68 ContextRootMissing,
69 ContextRootMismatch,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum LocationSegment {
74 Member(String),
75 Index(usize),
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Default)]
79pub struct ObjectLocation {
80 segments: Vec<LocationSegment>,
81}
82
83impl ObjectLocation {
84 pub fn root() -> Self {
85 Self::default()
86 }
87
88 pub fn hash_root(member: impl Into<String>) -> Self {
89 Self::root().member(member)
90 }
91
92 pub fn array_root(index: usize) -> Self {
93 Self::root().element(index)
94 }
95
96 pub fn member(mut self, member: impl Into<String>) -> Self {
97 self.segments.push(LocationSegment::Member(member.into()));
98 self
99 }
100
101 pub fn element(mut self, index: usize) -> Self {
102 self.segments.push(LocationSegment::Index(index));
103 self
104 }
105
106 pub fn is_root(&self) -> bool {
107 self.segments.is_empty()
108 }
109
110 pub fn level(&self) -> usize {
111 self.segments.len()
112 }
113}
114
115impl std::fmt::Display for ObjectLocation {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 if self.segments.is_empty() {
118 return write!(f, "$");
119 }
120 let mut first = true;
121 for segment in &self.segments {
122 match segment {
123 LocationSegment::Member(member) => {
124 if !first {
125 write!(f, ".")?;
126 }
127 write!(f, "{member}")?;
128 }
129 LocationSegment::Index(index) => {
130 write!(f, "[{index}]")?;
131 }
132 }
133 first = false;
134 }
135 Ok(())
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
140pub struct CheckResult {
141 pub rule: CheckRule,
142 pub location: ObjectLocation,
143 pub input_value: Option<Value>,
144 pub system_value: Option<Value>,
145 pub message: Option<String>,
146}
147
148impl CheckResult {
149 pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
150 Self {
151 rule,
152 location,
153 input_value: None,
154 system_value: None,
155 message: None,
156 }
157 }
158
159 pub fn required(location: ObjectLocation) -> Self {
160 Self::new(CheckRule::Required, location)
161 }
162
163 pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
164 Self::new(CheckRule::Min, location)
165 .with_system_value(min)
166 .with_input_value(current)
167 }
168
169 pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
170 Self::new(CheckRule::Max, location)
171 .with_system_value(max)
172 .with_input_value(current)
173 }
174
175 pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
176 Self::new(CheckRule::MinStringLength, location)
177 .with_system_value(min_len)
178 .with_input_value(current)
179 }
180
181 pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
182 Self::new(CheckRule::MaxStringLength, location)
183 .with_system_value(max_len)
184 .with_input_value(current)
185 }
186
187 pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
188 self.input_value = Some(value.into());
189 self
190 }
191
192 pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
193 self.system_value = Some(value.into());
194 self
195 }
196
197 pub fn with_message(mut self, message: impl Into<String>) -> Self {
198 self.message = Some(message.into());
199 self
200 }
201}
202
203impl std::fmt::Display for CheckResult {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 match &self.message {
206 Some(message) => write!(f, "{message}"),
207 None => write!(f, "{}: {:?}", self.location, self.rule),
208 }
209 }
210}
211
212pub type CheckResults = Vec<CheckResult>;
213
214pub trait Checker: Send + Sync {
215 fn entity(&self) -> &str;
216
217 fn check_and_fix(
218 &self,
219 context: &UserContext,
220 values: &mut EntityValues,
221 location: &ObjectLocation,
222 results: &mut CheckResults,
223 );
224
225 fn required(
226 &self,
227 values: &EntityValues,
228 field: &str,
229 location: &ObjectLocation,
230 results: &mut CheckResults,
231 ) {
232 if matches!(values.get(field), None | Some(Value::Null)) {
233 results.push(CheckResult::required(location.clone().member(field)));
234 }
235 }
236
237 fn min_string_length(
238 &self,
239 values: &EntityValues,
240 field: &str,
241 min_len: usize,
242 location: &ObjectLocation,
243 results: &mut CheckResults,
244 ) {
245 if let Some(Value::Text(value)) = values.get(field) {
246 if value.chars().count() < min_len {
247 results.push(CheckResult::min_str(
248 location.clone().member(field),
249 min_len as u64,
250 value.clone(),
251 ));
252 }
253 }
254 }
255
256 fn max_string_length(
257 &self,
258 values: &EntityValues,
259 field: &str,
260 max_len: usize,
261 location: &ObjectLocation,
262 results: &mut CheckResults,
263 ) {
264 if let Some(Value::Text(value)) = values.get(field) {
265 if value.chars().count() > max_len {
266 results.push(CheckResult::max_str(
267 location.clone().member(field),
268 max_len as u64,
269 value.clone(),
270 ));
271 }
272 }
273 }
274}
275
276pub trait CheckerRegistry: Send + Sync {
277 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
278}
279
280#[derive(Default, Clone)]
281pub struct InMemoryCheckerRegistry {
282 checkers: BTreeMap<String, Arc<dyn Checker>>,
283}
284
285impl InMemoryCheckerRegistry {
286 pub fn new() -> Self {
287 Self::default()
288 }
289
290 pub fn register(&mut self, checker: impl Checker + 'static) {
291 self.checkers
292 .insert(checker.entity().to_owned(), Arc::new(checker));
293 }
294
295 pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
296 self.register(checker);
297 self
298 }
299}
300
301impl CheckerRegistry for InMemoryCheckerRegistry {
302 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
303 self.checkers.get(entity).cloned()
304 }
305}
306
307pub trait TypedChecker<T>: Send + Sync {
318 fn check_and_fix_typed(
319 &self,
320 context: &UserContext,
321 entity: &mut T,
322 status: CheckObjectStatus,
323 location: &ObjectLocation,
324 results: &mut CheckResults,
325 );
326}
327
328pub struct TypedEntityChecker<T, C> {
336 checker: C,
337 entity_name: String,
338 _marker: std::marker::PhantomData<fn() -> T>,
339}
340
341impl<T, C> TypedEntityChecker<T, C>
342where
343 T: TeaqlEntity,
344{
345 pub fn new(checker: C) -> Self {
347 let entity_name = T::entity_descriptor().name.clone();
348 Self {
349 checker,
350 entity_name,
351 _marker: std::marker::PhantomData,
352 }
353 }
354}
355
356impl<T, C> Checker for TypedEntityChecker<T, C>
357where
358 T: Entity + TeaqlEntity + Send + Sync + Clone,
359 C: TypedChecker<T>,
360{
361 fn entity(&self) -> &str {
362 &self.entity_name
363 }
364
365 fn check_and_fix(
366 &self,
367 context: &UserContext,
368 values: &mut EntityValues,
369 location: &ObjectLocation,
370 results: &mut CheckResults,
371 ) {
372 let status = CheckObjectStatus::from_values(values);
373 let original_values = std::mem::take(values);
378 let owned_record = original_values.clone().into();
379 match T::from_compact_row(teaql_core::CompactRow::from_map(owned_record)) {
380 Ok(mut entity) => {
381 let before_check = entity.clone().into_values();
382 self.checker
383 .check_and_fix_typed(context, &mut entity, status, location, results);
384 let after_check = entity.into_values();
385 *values = original_values;
386 for (field, after_value) in after_check {
387 if before_check.get(&field) != Some(&after_value) {
388 values.insert(field, after_value);
389 }
390 }
391 }
392 Err(error) => {
393 *values = original_values;
397 results.push(
398 CheckResult::new(CheckRule::InvalidType, location.clone()).with_message(
399 format!(
400 "failed to materialize {} for checker: {error}",
401 self.entity_name
402 ),
403 ),
404 );
405 }
406 }
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_object_location_formatting_and_nesting_levels() {
416 let root = ObjectLocation::root();
418 assert_eq!(root.to_string(), "$");
419 assert!(root.is_root());
420 assert_eq!(root.level(), 0);
421
422 let hash = ObjectLocation::hash_root("user");
424 assert_eq!(hash.to_string(), "user");
425 assert!(!hash.is_root());
426 assert_eq!(hash.level(), 1);
427
428 let arr = ObjectLocation::array_root(5);
430 assert_eq!(arr.to_string(), "[5]");
431 assert!(!arr.is_root());
432 assert_eq!(arr.level(), 1);
433
434 let nested = ObjectLocation::root()
436 .member("users")
437 .element(2)
438 .member("address")
439 .member("city");
440
441 assert_eq!(nested.to_string(), "users[2].address.city");
442 assert_eq!(nested.level(), 4);
443 }
444
445 #[test]
446 fn test_check_object_status_inference_and_explicit_markers() {
447 let mut values = EntityValues::default();
448
449 assert_eq!(
451 CheckObjectStatus::from_values(&values),
452 CheckObjectStatus::Create
453 );
454
455 values.insert("id".to_string(), Value::I64(1));
457 assert_eq!(
458 CheckObjectStatus::from_values(&values),
459 CheckObjectStatus::Update
460 );
461
462 mark_entity_status(&mut values, CheckObjectStatus::Create);
464 assert_eq!(
465 CheckObjectStatus::from_values(&values),
466 CheckObjectStatus::Create
467 );
468
469 mark_entity_status(&mut values, CheckObjectStatus::Update);
471 assert_eq!(
472 CheckObjectStatus::from_values(&values),
473 CheckObjectStatus::Update
474 );
475
476 clear_entity_status(&mut values);
478 assert_eq!(
479 CheckObjectStatus::from_values(&values),
480 CheckObjectStatus::Update
481 ); }
483}