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}