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
306pub 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
327pub 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 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 let owned_record = std::mem::take(values).into();
375 match T::from_compact_row(teaql_core::CompactRow::from_map(owned_record)) {
376 Ok(mut entity) => {
377 self.checker
378 .check_and_fix_typed(context, &mut entity, status, location, results);
379 *values = entity.into_values().into();
381 }
382 Err(_e) => {
383 *values = EntityValues::default();
386 results.push(CheckResult::new(CheckRule::Required, location.clone()));
388 }
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_object_location_formatting_and_nesting_levels() {
399 let root = ObjectLocation::root();
401 assert_eq!(root.to_string(), "$");
402 assert!(root.is_root());
403 assert_eq!(root.level(), 0);
404
405 let hash = ObjectLocation::hash_root("user");
407 assert_eq!(hash.to_string(), "user");
408 assert!(!hash.is_root());
409 assert_eq!(hash.level(), 1);
410
411 let arr = ObjectLocation::array_root(5);
413 assert_eq!(arr.to_string(), "[5]");
414 assert!(!arr.is_root());
415 assert_eq!(arr.level(), 1);
416
417 let nested = ObjectLocation::root()
419 .member("users")
420 .element(2)
421 .member("address")
422 .member("city");
423
424 assert_eq!(nested.to_string(), "users[2].address.city");
425 assert_eq!(nested.level(), 4);
426 }
427
428 #[test]
429 fn test_check_object_status_inference_and_explicit_markers() {
430 let mut values = EntityValues::default();
431
432 assert_eq!(
434 CheckObjectStatus::from_values(&values),
435 CheckObjectStatus::Create
436 );
437
438 values.insert("id".to_string(), Value::I64(1));
440 assert_eq!(
441 CheckObjectStatus::from_values(&values),
442 CheckObjectStatus::Update
443 );
444
445 mark_entity_status(&mut values, CheckObjectStatus::Create);
447 assert_eq!(
448 CheckObjectStatus::from_values(&values),
449 CheckObjectStatus::Create
450 );
451
452 mark_entity_status(&mut values, CheckObjectStatus::Update);
454 assert_eq!(
455 CheckObjectStatus::from_values(&values),
456 CheckObjectStatus::Update
457 );
458
459 clear_entity_status(&mut values);
461 assert_eq!(
462 CheckObjectStatus::from_values(&values),
463 CheckObjectStatus::Update
464 ); }
466}