1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use teaql_core::{Entity, Record, TeaqlEntity, Value};
5
6use crate::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_record(record: &Record) -> Self {
27 match record.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 record.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_record_status(record: &mut Record, status: CheckObjectStatus) {
53 record.insert(CHECK_OBJECT_STATUS_FIELD.to_owned(), status.into());
54}
55
56pub fn clear_record_status(record: &mut Record) {
57 record.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}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum LocationSegment {
71 Member(String),
72 Index(usize),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Default)]
76pub struct ObjectLocation {
77 segments: Vec<LocationSegment>,
78}
79
80impl ObjectLocation {
81 pub fn root() -> Self {
82 Self::default()
83 }
84
85 pub fn hash_root(member: impl Into<String>) -> Self {
86 Self::root().member(member)
87 }
88
89 pub fn array_root(index: usize) -> Self {
90 Self::root().element(index)
91 }
92
93 pub fn member(mut self, member: impl Into<String>) -> Self {
94 self.segments.push(LocationSegment::Member(member.into()));
95 self
96 }
97
98 pub fn element(mut self, index: usize) -> Self {
99 self.segments.push(LocationSegment::Index(index));
100 self
101 }
102
103 pub fn is_root(&self) -> bool {
104 self.segments.is_empty()
105 }
106
107 pub fn level(&self) -> usize {
108 self.segments.len()
109 }
110}
111
112impl std::fmt::Display for ObjectLocation {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 if self.segments.is_empty() {
115 return write!(f, "$");
116 }
117 let mut first = true;
118 for segment in &self.segments {
119 match segment {
120 LocationSegment::Member(member) => {
121 if !first {
122 write!(f, ".")?;
123 }
124 write!(f, "{member}")?;
125 }
126 LocationSegment::Index(index) => {
127 write!(f, "[{index}]")?;
128 }
129 }
130 first = false;
131 }
132 Ok(())
133 }
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct CheckResult {
138 pub rule: CheckRule,
139 pub location: ObjectLocation,
140 pub input_value: Option<Value>,
141 pub system_value: Option<Value>,
142 pub message: Option<String>,
143}
144
145impl CheckResult {
146 pub fn new(rule: CheckRule, location: ObjectLocation) -> Self {
147 Self {
148 rule,
149 location,
150 input_value: None,
151 system_value: None,
152 message: None,
153 }
154 }
155
156 pub fn required(location: ObjectLocation) -> Self {
157 Self::new(CheckRule::Required, location)
158 }
159
160 pub fn min(location: ObjectLocation, min: impl Into<Value>, current: impl Into<Value>) -> Self {
161 Self::new(CheckRule::Min, location)
162 .with_system_value(min)
163 .with_input_value(current)
164 }
165
166 pub fn max(location: ObjectLocation, max: impl Into<Value>, current: impl Into<Value>) -> Self {
167 Self::new(CheckRule::Max, location)
168 .with_system_value(max)
169 .with_input_value(current)
170 }
171
172 pub fn min_str(location: ObjectLocation, min_len: u64, current: impl Into<Value>) -> Self {
173 Self::new(CheckRule::MinStringLength, location)
174 .with_system_value(min_len)
175 .with_input_value(current)
176 }
177
178 pub fn max_str(location: ObjectLocation, max_len: u64, current: impl Into<Value>) -> Self {
179 Self::new(CheckRule::MaxStringLength, location)
180 .with_system_value(max_len)
181 .with_input_value(current)
182 }
183
184 pub fn with_input_value(mut self, value: impl Into<Value>) -> Self {
185 self.input_value = Some(value.into());
186 self
187 }
188
189 pub fn with_system_value(mut self, value: impl Into<Value>) -> Self {
190 self.system_value = Some(value.into());
191 self
192 }
193
194 pub fn with_message(mut self, message: impl Into<String>) -> Self {
195 self.message = Some(message.into());
196 self
197 }
198}
199
200impl std::fmt::Display for CheckResult {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 match &self.message {
203 Some(message) => write!(f, "{message}"),
204 None => write!(f, "{}: {:?}", self.location, self.rule),
205 }
206 }
207}
208
209pub type CheckResults = Vec<CheckResult>;
210
211pub trait Checker: Send + Sync {
212 fn entity(&self) -> &str;
213
214 fn check_and_fix(
215 &self,
216 ctx: &UserContext,
217 record: &mut Record,
218 location: &ObjectLocation,
219 results: &mut CheckResults,
220 );
221
222 fn required(
223 &self,
224 record: &Record,
225 field: &str,
226 location: &ObjectLocation,
227 results: &mut CheckResults,
228 ) {
229 if matches!(record.get(field), None | Some(Value::Null)) {
230 results.push(CheckResult::required(location.clone().member(field)));
231 }
232 }
233
234 fn min_string_length(
235 &self,
236 record: &Record,
237 field: &str,
238 min_len: usize,
239 location: &ObjectLocation,
240 results: &mut CheckResults,
241 ) {
242 if let Some(Value::Text(value)) = record.get(field) {
243 if value.chars().count() < min_len {
244 results.push(CheckResult::min_str(
245 location.clone().member(field),
246 min_len as u64,
247 value.clone(),
248 ));
249 }
250 }
251 }
252
253 fn max_string_length(
254 &self,
255 record: &Record,
256 field: &str,
257 max_len: usize,
258 location: &ObjectLocation,
259 results: &mut CheckResults,
260 ) {
261 if let Some(Value::Text(value)) = record.get(field) {
262 if value.chars().count() > max_len {
263 results.push(CheckResult::max_str(
264 location.clone().member(field),
265 max_len as u64,
266 value.clone(),
267 ));
268 }
269 }
270 }
271}
272
273pub trait CheckerRegistry: Send + Sync {
274 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>>;
275}
276
277#[derive(Default, Clone)]
278pub struct InMemoryCheckerRegistry {
279 checkers: BTreeMap<String, Arc<dyn Checker>>,
280}
281
282impl InMemoryCheckerRegistry {
283 pub fn new() -> Self {
284 Self::default()
285 }
286
287 pub fn register(&mut self, checker: impl Checker + 'static) {
288 self.checkers
289 .insert(checker.entity().to_owned(), Arc::new(checker));
290 }
291
292 pub fn with_checker(mut self, checker: impl Checker + 'static) -> Self {
293 self.register(checker);
294 self
295 }
296}
297
298impl CheckerRegistry for InMemoryCheckerRegistry {
299 fn checker(&self, entity: &str) -> Option<Arc<dyn Checker>> {
300 self.checkers.get(entity).cloned()
301 }
302}
303
304pub trait TypedChecker<T>: Send + Sync {
315 fn check_and_fix_typed(
316 &self,
317 ctx: &UserContext,
318 entity: &mut T,
319 status: CheckObjectStatus,
320 location: &ObjectLocation,
321 results: &mut CheckResults,
322 );
323}
324
325pub struct TypedEntityChecker<T, C> {
334 checker: C,
335 entity_name: String,
336 _marker: std::marker::PhantomData<fn() -> T>,
337}
338
339impl<T, C> TypedEntityChecker<T, C>
340where
341 T: TeaqlEntity,
342{
343 pub fn new(checker: C) -> Self {
345 let entity_name = T::entity_descriptor().name.clone();
346 Self {
347 checker,
348 entity_name,
349 _marker: std::marker::PhantomData,
350 }
351 }
352}
353
354impl<T, C> Checker for TypedEntityChecker<T, C>
355where
356 T: Entity + TeaqlEntity + Send + Sync + Clone,
357 C: TypedChecker<T>,
358{
359 fn entity(&self) -> &str {
360 &self.entity_name
361 }
362
363 fn check_and_fix(
364 &self,
365 ctx: &UserContext,
366 record: &mut Record,
367 location: &ObjectLocation,
368 results: &mut CheckResults,
369 ) {
370 let status = CheckObjectStatus::from_record(record);
371 let owned_record = std::mem::take(record);
374 match T::from_record(owned_record) {
375 Ok(mut entity) => {
376 self.checker
377 .check_and_fix_typed(ctx, &mut entity, status, location, results);
378 *record = entity.into_record();
380 }
381 Err(_e) => {
382 *record = Record::default();
385 results.push(CheckResult::new(CheckRule::Required, location.clone()));
387 }
388 }
389 }
390}