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 pub fn model_path(&self) -> String {
116 self.render_path(|name| name.to_owned())
117 }
118
119 pub fn native_path(&self) -> String {
121 self.render_path(|name| name.to_owned())
122 }
123
124 pub fn instance_path(&self) -> String {
126 self.segments
127 .iter()
128 .map(|segment| match segment {
129 LocationSegment::Member(member) => {
130 format!("/{}", escape_json_pointer(&lower_camel(member)))
131 }
132 LocationSegment::Index(index) => format!("/{index}"),
133 })
134 .collect()
135 }
136
137 fn render_path(&self, property_name: impl Fn(&str) -> String) -> String {
138 let mut result = String::new();
139 for segment in &self.segments {
140 match segment {
141 LocationSegment::Member(member) => {
142 if !result.is_empty() {
143 result.push('.');
144 }
145 result.push_str(&property_name(member));
146 }
147 LocationSegment::Index(index) => result.push_str(&format!("[{index}]")),
148 }
149 }
150 result
151 }
152}
153
154fn lower_camel(name: &str) -> String {
155 let mut parts = name.split('_');
156 let mut result = parts.next().unwrap_or_default().to_owned();
157 for part in parts {
158 let mut chars = part.chars();
159 if let Some(first) = chars.next() {
160 result.extend(first.to_uppercase());
161 result.extend(chars);
162 }
163 }
164 result
165}
166
167fn escape_json_pointer(value: &str) -> String {
168 value.replace('~', "~0").replace('/', "~1")
169}
170
171impl std::fmt::Display for ObjectLocation {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 if self.segments.is_empty() {
174 return write!(f, "$");
175 }
176 let mut first = true;
177 for segment in &self.segments {
178 match segment {
179 LocationSegment::Member(member) => {
180 if !first {
181 write!(f, ".")?;
182 }
183 write!(f, "{member}")?;
184 }
185 LocationSegment::Index(index) => {
186 write!(f, "[{index}]")?;
187 }
188 }
189 first = false;
190 }
191 Ok(())
192 }
193}
194
195#[derive(Debug, Clone, PartialEq)]
196pub struct CheckResult {
197 pub rule: CheckRule,
198 pub location: ObjectLocation,
199 pub input_value: Option<Value>,
200 pub system_value: Option<Value>,
201 pub message: Option<String>,
202}
203
204impl CheckResult {
205 pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
206 Self {
207 rule,
208 location,
209 input_value: None,
210 system_value: None,
211 message: None,
212 }
213 }
214
215 pub fn required(location: ObjectLocation) -> Self {
216 Self::new(CheckRule::Required, location)
217 }
218
219 pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
220 Self::new(CheckRule::Min, location)
221 .with_system_value(min)
222 .with_input_value(current)
223 }
224
225 pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
226 Self::new(CheckRule::Max, location)
227 .with_system_value(max)
228 .with_input_value(current)
229 }
230
231 pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
232 Self::new(CheckRule::MinStringLength, location)
233 .with_system_value(min_len)
234 .with_input_value(current)
235 }
236
237 pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
238 Self::new(CheckRule::MaxStringLength, location)
239 .with_system_value(max_len)
240 .with_input_value(current)
241 }
242
243 pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
244 self.input_value = Some(value.into());
245 self
246 }
247
248 pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
249 self.system_value = Some(value.into());
250 self
251 }
252
253 pub fn with_message(mut self, message: impl Into<String>) -> Self {
254 self.message = Some(message.into());
255 self
256 }
257}
258
259impl std::fmt::Display for CheckResult {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match &self.message {
262 Some(message) => write!(f, "{message}"),
263 None => write!(f, "{}: {:?}", self.location, self.rule),
264 }
265 }
266}
267
268pub type CheckResults = Vec<CheckResult>;
269
270pub trait Checker: Send + Sync {
271 fn entity(&self) -> &str;
272
273 fn check_and_fix(
274 &self,
275 context: &UserContext,
276 values: &mut EntityValues,
277 location: &ObjectLocation,
278 results: &mut CheckResults,
279 );
280
281 fn required(
282 &self,
283 values: &EntityValues,
284 field: &str,
285 location: &ObjectLocation,
286 results: &mut CheckResults,
287 ) {
288 if matches!(values.get(field), None | Some(Value::Null)) {
289 results.push(CheckResult::required(location.clone().member(field)));
290 }
291 }
292
293 fn min_string_length(
294 &self,
295 values: &EntityValues,
296 field: &str,
297 min_len: usize,
298 location: &ObjectLocation,
299 results: &mut CheckResults,
300 ) {
301 if let Some(Value::Text(value)) = values.get(field) {
302 if value.chars().count() < min_len {
303 results.push(CheckResult::min_str(
304 location.clone().member(field),
305 min_len as u64,
306 value.clone(),
307 ));
308 }
309 }
310 }
311
312 fn max_string_length(
313 &self,
314 values: &EntityValues,
315 field: &str,
316 max_len: usize,
317 location: &ObjectLocation,
318 results: &mut CheckResults,
319 ) {
320 if let Some(Value::Text(value)) = values.get(field) {
321 if value.chars().count() > max_len {
322 results.push(CheckResult::max_str(
323 location.clone().member(field),
324 max_len as u64,
325 value.clone(),
326 ));
327 }
328 }
329 }
330}
331
332pub trait CheckerRegistry: Send + Sync {
333 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
334}
335
336#[derive(Default, Clone)]
337pub struct InMemoryCheckerRegistry {
338 checkers: BTreeMap<String, Arc<dyn Checker>>,
339}
340
341impl InMemoryCheckerRegistry {
342 pub fn new() -> Self {
343 Self::default()
344 }
345
346 pub fn register(&mut self, checker: impl Checker + 'static) {
347 self.checkers
348 .insert(checker.entity().to_owned(), Arc::new(checker));
349 }
350
351 pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
352 self.register(checker);
353 self
354 }
355}
356
357impl CheckerRegistry for InMemoryCheckerRegistry {
358 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
359 self.checkers.get(entity).cloned()
360 }
361}
362
363pub trait TypedChecker<T>: Send + Sync {
374 fn check_and_fix_typed(
375 &self,
376 context: &UserContext,
377 entity: &mut T,
378 status: CheckObjectStatus,
379 location: &ObjectLocation,
380 results: &mut CheckResults,
381 );
382}
383
384pub struct TypedEntityChecker<T, C> {
392 checker: C,
393 entity_name: String,
394 _marker: std::marker::PhantomData<fn() -> T>,
395}
396
397impl<T, C> TypedEntityChecker<T, C>
398where
399 T: TeaqlEntity,
400{
401 pub fn new(checker: C) -> Self {
403 let entity_name = T::entity_descriptor().name.clone();
404 Self {
405 checker,
406 entity_name,
407 _marker: std::marker::PhantomData,
408 }
409 }
410}
411
412impl<T, C> Checker for TypedEntityChecker<T, C>
413where
414 T: Entity + TeaqlEntity + Send + Sync + Clone,
415 C: TypedChecker<T>,
416{
417 fn entity(&self) -> &str {
418 &self.entity_name
419 }
420
421 fn check_and_fix(
422 &self,
423 context: &UserContext,
424 values: &mut EntityValues,
425 location: &ObjectLocation,
426 results: &mut CheckResults,
427 ) {
428 let status = CheckObjectStatus::from_values(values);
429 let mut original_values = std::mem::take(values);
434 let loaded_fields = match original_values.remove("_loaded_fields") {
435 Some(Value::List(fields)) => Some(
436 fields
437 .into_iter()
438 .filter_map(|field| match field {
439 Value::Text(field) => Some(field),
440 _ => None,
441 })
442 .collect::<std::collections::BTreeSet<_>>(),
443 ),
444 _ => None,
445 };
446 let owned_record = original_values.clone().into();
447 match T::from_compact_row(teaql_core::CompactRow::from_map(owned_record)) {
448 Ok(mut entity) => {
449 if let Some(loaded_fields) = loaded_fields {
450 entity.set_checker_loaded_fields(loaded_fields);
451 }
452 let before_check = entity.clone().into_values();
453 self.checker
454 .check_and_fix_typed(context, &mut entity, status, location, results);
455 let after_check = entity.into_values();
456 let descriptor = T::entity_descriptor();
457 for property in descriptor
458 .properties
459 .iter()
460 .filter(|property| !property.nullable && !property.is_id && !property.is_version)
461 {
462 let was_absent_or_null = original_values
463 .get(&property.name)
464 .is_none_or(|value| matches!(value, Value::Null));
465 if was_absent_or_null
466 && after_check
467 .get(&property.name)
468 .is_none_or(|value| matches!(value, Value::Null) || before_check.get(&property.name) == Some(value))
469 {
470 results.push(CheckResult::required(
471 location.clone().member(&property.name),
472 ));
473 }
474 }
475 *values = original_values;
476 for (field, after_value) in after_check {
477 if before_check.get(&field) != Some(&after_value) {
478 values.insert(field, after_value);
479 }
480 }
481 }
482 Err(error) => {
483 *values = original_values;
487 results.push(
488 CheckResult::new(CheckRule::InvalidType, location.clone()).with_message(
489 format!(
490 "failed to materialize {} for checker: {error}",
491 self.entity_name
492 ),
493 ),
494 );
495 }
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn test_object_location_formatting_and_nesting_levels() {
506 let root = ObjectLocation::root();
508 assert_eq!(root.to_string(), "$");
509 assert!(root.is_root());
510 assert_eq!(root.level(), 0);
511
512 let hash = ObjectLocation::hash_root("user");
514 assert_eq!(hash.to_string(), "user");
515 assert!(!hash.is_root());
516 assert_eq!(hash.level(), 1);
517
518 let arr = ObjectLocation::array_root(5);
520 assert_eq!(arr.to_string(), "[5]");
521 assert!(!arr.is_root());
522 assert_eq!(arr.level(), 1);
523
524 let nested = ObjectLocation::root()
526 .member("users")
527 .element(2)
528 .member("address")
529 .member("city");
530
531 assert_eq!(nested.to_string(), "users[2].address.city");
532 assert_eq!(nested.level(), 4);
533 }
534
535 #[test]
536 fn object_location_renders_model_native_and_external_paths() {
537 let location = ObjectLocation::hash_root("order_items")
538 .element(2)
539 .member("user_url");
540
541 assert_eq!(location.model_path(), "order_items[2].user_url");
542 assert_eq!(location.native_path(), "order_items[2].user_url");
543 assert_eq!(location.instance_path(), "/orderItems/2/userUrl");
544 assert_eq!(location.to_string(), "order_items[2].user_url");
545 }
546
547 #[test]
548 fn object_location_escapes_json_pointer_members() {
549 assert_eq!(ObjectLocation::hash_root("a~/b").instance_path(), "/a~0~1b");
550 }
551
552 #[test]
553 fn test_check_object_status_inference_and_explicit_markers() {
554 let mut values = EntityValues::default();
555
556 assert_eq!(
558 CheckObjectStatus::from_values(&values),
559 CheckObjectStatus::Create
560 );
561
562 values.insert("id".to_string(), Value::I64(1));
564 assert_eq!(
565 CheckObjectStatus::from_values(&values),
566 CheckObjectStatus::Update
567 );
568
569 mark_entity_status(&mut values, CheckObjectStatus::Create);
571 assert_eq!(
572 CheckObjectStatus::from_values(&values),
573 CheckObjectStatus::Create
574 );
575
576 mark_entity_status(&mut values, CheckObjectStatus::Update);
578 assert_eq!(
579 CheckObjectStatus::from_values(&values),
580 CheckObjectStatus::Update
581 );
582
583 clear_entity_status(&mut values);
585 assert_eq!(
586 CheckObjectStatus::from_values(&values),
587 CheckObjectStatus::Update
588 ); }
590}