1use super::JSONEval;
2use crate::jsoneval::cancellation::CancellationToken;
3use crate::jsoneval::json_parser;
4use crate::jsoneval::path_utils;
5use crate::jsoneval::types::{ValidationError, ValidationResult};
6
7use crate::time_block;
8
9use indexmap::IndexMap;
10use serde_json::Value;
11
12impl JSONEval {
13 pub(crate) fn invalidate_validation_cache(&self) {
15 let mut cache = match self.validation_cache.write() {
16 Ok(c) => c,
17 Err(poisoned) => poisoned.into_inner(),
18 };
19 cache.clear();
20 }
21
22 pub fn validate(
24 &mut self,
25 data: &str,
26 context: Option<&str>,
27 paths: Option<&[String]>,
28 token: Option<&CancellationToken>,
29 validate_readonly: Option<bool>,
30 ) -> Result<ValidationResult, String> {
31 let validate_ro = validate_readonly.unwrap_or(false);
32 if let Some(t) = token {
33 if t.is_cancelled() {
34 return Err("Cancelled".to_string());
35 }
36 }
37
38 if paths.is_none() || paths.is_some_and(|p| p.is_empty()) {
41 if let Ok(cache) = self.validation_cache.read() {
42 if let Some(cached) = cache.get_cached_full_result(data, context, validate_ro) {
43 return Ok(cached);
44 }
45 }
46 }
47
48 time_block!("validate() [total]", {
49 let _lock = self.eval_lock.lock().unwrap();
51
52 let (data_value, context_value) = time_block!(" parse data & context", {
54 let d = json_parser::parse_json_str(data)?;
55 let c = if let Some(ctx) = context {
56 json_parser::parse_json_str(ctx)?
57 } else {
58 Value::Object(serde_json::Map::new())
59 };
60 Ok::<_, String>((d, c))
61 })?;
62
63 self.context = context_value.clone();
65
66 time_block!(" replace_data_and_context", {
68 self.eval_data
69 .replace_data_and_context(data_value.clone(), context_value);
70 });
71
72 drop(_lock);
74
75 time_block!(" evaluate_others", {
78 self.evaluate_others(paths, token);
79 });
80
81 time_block!(" ensure_layout_resolved", {
82 self.ensure_layout_resolved();
83 });
84
85 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
86
87 let layout_state = self.layout_state.read().unwrap();
88 let layout_hidden_refs = &layout_state.layout_hidden_refs;
89 let layout_disabled_refs = &layout_state.layout_disabled_refs;
90 let mut hidden_cache =
91 std::collections::HashMap::with_capacity(self.fields_with_rules.len());
92 let mut readonly_cache =
93 std::collections::HashMap::with_capacity(self.fields_with_rules.len());
94
95 time_block!(" fields_with_rules loop", {
98 for field_path in self.fields_with_rules.iter() {
99 if let Some(filter_paths) = paths {
101 if !filter_paths.is_empty()
102 && !filter_paths.iter().any(|p| {
103 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
104 })
105 {
106 continue;
107 }
108 }
109
110 self.validate_field_cached(
111 field_path,
112 &data_value,
113 layout_hidden_refs,
114 layout_disabled_refs,
115 &mut hidden_cache,
116 &mut readonly_cache,
117 validate_ro,
118 &mut errors,
119 );
120
121 if let Some(t) = token {
122 if t.is_cancelled() {
123 return Err("Cancelled".to_string());
124 }
125 }
126 }
127 });
128
129 drop(layout_state);
130
131 let has_error = !errors.is_empty();
132 let result = ValidationResult { has_error, errors };
133
134 if paths.is_none() || paths.is_some_and(|p| p.is_empty()) {
135 if let Ok(mut cache) = self.validation_cache.write() {
136 cache.save_full_result(
137 data.to_string(),
138 context.map(|s| s.to_string()),
139 validate_ro,
140 result.clone(),
141 );
142 }
143 } else if let Ok(mut cache) = self.validation_cache.write() {
144 cache.invalidate_full_result();
145 }
146
147 Ok(result)
148 })
149 }
150
151 pub(crate) fn validate_pre_set(
156 &mut self,
157 data_value: Value,
158 paths: Option<&[String]>,
159 token: Option<&CancellationToken>,
160 validate_readonly: Option<bool>,
161 ) -> Result<crate::ValidationResult, String> {
162 let validate_ro = validate_readonly.unwrap_or(false);
163 self.evaluate_others(paths, token);
165
166 self.ensure_layout_resolved();
167
168 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
169
170 let layout_state = self.layout_state.read().unwrap();
171 let layout_hidden_refs = &layout_state.layout_hidden_refs;
172 let layout_disabled_refs = &layout_state.layout_disabled_refs;
173 let mut hidden_cache =
174 std::collections::HashMap::with_capacity(self.fields_with_rules.len());
175 let mut readonly_cache =
176 std::collections::HashMap::with_capacity(self.fields_with_rules.len());
177
178 for field_path in self.fields_with_rules.iter() {
179 if let Some(filter_paths) = paths {
180 if !filter_paths.is_empty()
181 && !filter_paths.iter().any(|p| {
182 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
183 })
184 {
185 continue;
186 }
187 }
188 if let Some(t) = token {
189 if t.is_cancelled() {
190 return Err("Cancelled".to_string());
191 }
192 }
193 self.validate_field_cached(
194 field_path,
195 &data_value,
196 layout_hidden_refs,
197 layout_disabled_refs,
198 &mut hidden_cache,
199 &mut readonly_cache,
200 validate_ro,
201 &mut errors,
202 );
203 }
204
205 drop(layout_state);
206
207 let has_error = !errors.is_empty();
208 Ok(crate::ValidationResult { has_error, errors })
209 }
210
211 #[allow(dead_code)]
213 pub(crate) fn validate_field(
214 &self,
215 field_path: &str,
216 data: &Value,
217 validate_readonly: bool,
218 errors: &mut IndexMap<String, ValidationError>,
219 ) {
220 let layout_state = self.layout_state.read().unwrap();
221 let mut hidden_cache = std::collections::HashMap::new();
222 let mut readonly_cache = std::collections::HashMap::new();
223 self.validate_field_cached(
224 field_path,
225 data,
226 &layout_state.layout_hidden_refs,
227 &layout_state.layout_disabled_refs,
228 &mut hidden_cache,
229 &mut readonly_cache,
230 validate_readonly,
231 errors,
232 );
233 }
234
235 #[allow(clippy::too_many_arguments)]
237 pub(crate) fn validate_field_cached(
238 &self,
239 field_path: &str,
240 data: &Value,
241 layout_hidden_refs: &indexmap::IndexSet<String>,
242 layout_disabled_refs: &indexmap::IndexSet<String>,
243 hidden_cache: &mut std::collections::HashMap<String, bool>,
244 readonly_cache: &mut std::collections::HashMap<String, bool>,
245 validate_readonly: bool,
246 errors: &mut IndexMap<String, ValidationError>,
247 ) {
248 if errors.contains_key(field_path) {
250 return;
251 }
252
253 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
255 let pointer_path = schema_path.trim_start_matches('#');
256
257 let (field_schema, resolved_path) = match self.evaluated_schema.pointer(pointer_path) {
259 Some(s) => (s, pointer_path.to_string()),
260 None => {
261 let alt_path = format!("/properties{}", pointer_path);
262 match self.evaluated_schema.pointer(&alt_path) {
263 Some(s) => (s, alt_path),
264 None => return,
265 }
266 }
267 };
268
269 let is_hidden = self.is_effective_hidden_with_cache(
271 &resolved_path,
272 layout_hidden_refs,
273 hidden_cache,
274 );
275 if is_hidden {
276 if let Ok(mut cache) = self.validation_cache.write() {
277 cache.update_field(
278 field_path.to_string(),
279 Value::Null,
280 true,
281 validate_readonly,
282 Value::Null,
283 None,
284 );
285 }
286 return;
287 }
288
289 if !validate_readonly {
291 let is_readonly = self.is_effective_readonly_with_cache(
292 &resolved_path,
293 layout_disabled_refs,
294 readonly_cache,
295 );
296 if is_readonly {
297 if let Ok(mut cache) = self.validation_cache.write() {
298 cache.update_field(
299 field_path.to_string(),
300 Value::Null,
301 false,
302 false,
303 Value::Null,
304 None,
305 );
306 }
307 return;
308 }
309 }
310
311 if let Value::Object(schema_map) = field_schema {
312 let rules_val = match schema_map.get("rules") {
314 Some(r @ Value::Object(_)) => r,
315 _ => return,
316 };
317 let rules = rules_val.as_object().unwrap();
318
319 let field_data = self.get_field_data(field_path, data);
321
322 let cached_lookup = if let Ok(cache) = self.validation_cache.read() {
324 cache.check_field_cache(field_path, &field_data, false, validate_readonly, rules_val)
325 } else {
326 None
327 };
328
329 if let Some(cached_error) = cached_lookup {
330 if let Some(err) = cached_error {
331 errors.insert(field_path.to_string(), err);
332 }
333 return;
334 }
335
336 let mut field_error: Option<ValidationError> = None;
338 time_block!(" validate_rules loop", {
339 for (rule_name, rule_value) in rules {
340 self.validate_rule(
341 field_path,
342 rule_name,
343 rule_value,
344 &field_data,
345 schema_map,
346 field_schema,
347 errors,
348 );
349 if let Some(err) = errors.get(field_path) {
350 field_error = Some(err.clone());
351 break;
352 }
353 }
354 });
355
356 if let Ok(mut cache) = self.validation_cache.write() {
357 cache.update_field(
358 field_path.to_string(),
359 field_data,
360 false,
361 validate_readonly,
362 rules_val.clone(),
363 field_error,
364 );
365 }
366 }
367 }
368
369 pub(crate) fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
371 let mut current = data;
372
373 for part in field_path.split('.') {
374 match current {
375 Value::Object(map) => {
376 current = map.get(part).unwrap_or(&Value::Null);
377 }
378 _ => return Value::Null,
379 }
380 }
381
382 current.clone()
383 }
384
385 #[allow(clippy::too_many_arguments)]
387 pub(crate) fn validate_rule(
388 &self,
389 field_path: &str,
390 rule_name: &str,
391 rule_value: &Value,
392 field_data: &Value,
393 schema_map: &serde_json::Map<String, Value>,
394 _schema: &Value,
395 errors: &mut IndexMap<String, ValidationError>,
396 ) {
397 if errors.contains_key(field_path) {
399 return;
400 }
401
402 let schema_type = schema_map
403 .get("type")
404 .and_then(|t| t.as_str())
405 .unwrap_or("");
406
407 let evaluated_rule = rule_value;
409
410 let (rule_active, rule_message, rule_code, rule_data) = match evaluated_rule {
413 Value::Object(rule_obj) => {
414 let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
415
416 let message = match rule_obj.get("message") {
418 Some(Value::String(s)) => s.clone(),
419 Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => msg_obj
420 .get("value")
421 .and_then(|v| v.as_str())
422 .unwrap_or("Validation failed")
423 .to_string(),
424 Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
425 None => "Validation failed".to_string(),
426 };
427
428 let code = rule_obj
429 .get("code")
430 .and_then(|c| c.as_str())
431 .map(|s| s.to_string());
432
433 let data = rule_obj.get("data").map(|d| {
435 if let Value::Object(data_obj) = d {
436 let mut cleaned_data = serde_json::Map::new();
437 for (key, value) in data_obj {
438 if let Value::Object(val_obj) = value {
440 if val_obj.len() == 1 && val_obj.contains_key("value") {
441 cleaned_data.insert(key.clone(), val_obj["value"].clone());
442 } else {
443 cleaned_data.insert(key.clone(), value.clone());
444 }
445 } else {
446 cleaned_data.insert(key.clone(), value.clone());
447 }
448 }
449 Value::Object(cleaned_data)
450 } else {
451 d.clone()
452 }
453 });
454
455 (active.clone(), message, code, data)
456 }
457 _ => (
458 evaluated_rule.clone(),
459 "Validation failed".to_string(),
460 None,
461 None,
462 ),
463 };
464
465 let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
467
468 let is_empty = matches!(field_data, Value::Null)
469 || (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty())
470 || (field_data.is_array() && field_data.as_array().unwrap().is_empty());
471
472 match rule_name {
473 "required" => {
474 if rule_active == Value::Bool(true) {
475 if is_empty {
476 errors.insert(
477 field_path.to_string(),
478 ValidationError {
479 rule_type: "required".to_string(),
480 message: rule_message,
481 code: error_code,
482 pattern: None,
483 field_value: None,
484 data: None,
485 },
486 );
487 }
488 }
489 }
490 "minLength" | "maxLength" | "minValue" | "maxValue" => {
491 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
492 errors.insert(
493 field_path.to_string(),
494 ValidationError {
495 rule_type: rule_name.to_string(),
496 message: rule_message,
497 code: error_code,
498 pattern: None,
499 field_value: None,
500 data: None,
501 },
502 );
503 }
504 }
505
506 "pattern" => {
507 if !is_empty {
508 if let Some(pattern) = rule_active.as_str() {
509 if let Some(text) = field_data.as_str() {
510 let cached_regex = if let Ok(cache) = self.regex_cache.read() {
511 cache.get(pattern).cloned()
512 } else {
513 None
514 };
515 let regex = match cached_regex {
516 Some(r) => r,
517 None => {
518 let mut cache = self.regex_cache.write().unwrap();
519 cache.entry(pattern.to_string()).or_insert_with(|| {
520 regex::Regex::new(pattern)
521 .unwrap_or_else(|_| regex::Regex::new("(?:)").unwrap())
522 }).clone()
523 }
524 };
525 if !regex.is_match(text) {
526 errors.insert(
527 field_path.to_string(),
528 ValidationError {
529 rule_type: "pattern".to_string(),
530 message: rule_message,
531 code: error_code,
532 pattern: Some(pattern.to_string()),
533 field_value: Some(text.to_string()),
534 data: None,
535 },
536 );
537 }
538 }
539 }
540 }
541 }
542 "evaluation" => {
543 if let Value::Array(eval_array) = evaluated_rule {
546 for (idx, eval_item) in eval_array.iter().enumerate() {
547 if let Value::Object(eval_obj) = eval_item {
548 let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
550
551 let is_falsy = match eval_result {
553 Value::Bool(false) => true,
554 Value::Null => true,
555 Value::Number(n) => n.as_f64() == Some(0.0),
556 Value::String(s) => s.is_empty(),
557 Value::Array(a) => a.is_empty(),
558 _ => false,
559 };
560
561 if is_falsy {
562 let eval_code = eval_obj
563 .get("code")
564 .and_then(|c| c.as_str())
565 .map(|s| s.to_string())
566 .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
567
568 let eval_message = eval_obj
569 .get("message")
570 .and_then(|m| m.as_str())
571 .unwrap_or("Validation failed")
572 .to_string();
573
574 let eval_data = eval_obj.get("data").cloned();
575
576 errors.insert(
577 field_path.to_string(),
578 ValidationError {
579 rule_type: "evaluation".to_string(),
580 message: eval_message,
581 code: eval_code,
582 pattern: None,
583 field_value: None,
584 data: eval_data,
585 },
586 );
587
588 break;
590 }
591 }
592 }
593 }
594 }
595 _ => {
596 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
597 errors.insert(
598 field_path.to_string(),
599 ValidationError {
600 rule_type: "evaluation".to_string(),
601 message: rule_message,
602 code: error_code,
603 pattern: None,
604 field_value: None,
605 data: rule_data,
606 },
607 );
608 }
609 }
610 }
611 }
612
613 pub(crate) fn dep_fails_schema_rules(
620 &self,
621 field_path: &str,
622 field_data: &Value,
623 scope_data: &Value,
624 ) -> bool {
625 let schema_pointer = path_utils::dot_notation_to_schema_pointer(field_path);
626 let pointer = schema_pointer.trim_start_matches('#');
627
628 let field_schema = match self.schema.pointer(pointer) {
629 Some(s) => s,
630 None => {
631 let alt_pointer = format!("/properties{}", pointer);
632 match self.schema.pointer(&alt_pointer) {
633 Some(s) => s,
634 None => return false,
635 }
636 }
637 };
638
639 let schema_map = match field_schema.as_object() {
640 Some(m) => m,
641 None => return false,
642 };
643
644 let rules = match schema_map.get("rules") {
645 Some(Value::Object(r)) => r,
646 _ => return false,
647 };
648
649 let schema_type = schema_map
650 .get("type")
651 .and_then(|t| t.as_str())
652 .unwrap_or("");
653
654 let is_empty = matches!(field_data, Value::Null)
655 || field_data.as_str().map_or(false, |s| s.is_empty())
656 || field_data.as_array().map_or(false, |a| a.is_empty());
657
658 for (rule_name, rule_value) in rules {
659 let rule_eval_key = format!("#{}/rules/{}", pointer, rule_name);
663 let rule_active: Value = if let Some(logic_id) = self.evaluations.get(&rule_eval_key) {
664 let empty_ctx = Value::Object(serde_json::Map::new());
665 self.engine
666 .run_with_context(logic_id, scope_data, &empty_ctx)
667 .unwrap_or(Value::Null)
668 } else {
669 match rule_value {
670 Value::Object(obj) => obj.get("value").cloned().unwrap_or(Value::Null),
671 other => other.clone(),
672 }
673 };
674
675 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
676 return true;
677 }
678 }
679
680 false
681 }
682}
683
684fn rule_value_fails(
693 rule_name: &str,
694 rule_active: &Value,
695 field_data: &Value,
696 is_empty: bool,
697 schema_type: &str,
698) -> bool {
699 let coerce_num = |v: &Value| -> Option<f64> {
700 if let Some(n) = v.as_f64() {
701 return Some(n);
702 }
703 if matches!(schema_type, "number" | "integer") {
704 if let Some(s) = v.as_str() {
705 return s.trim().parse::<f64>().ok();
706 }
707 }
708 None
709 };
710
711 match rule_name {
712 "required" => is_empty && matches!(rule_active, Value::Bool(true)),
713 "minLength" => {
714 if is_empty {
715 false
716 } else if let Some(min) = rule_active.as_u64() {
717 let len = match field_data {
718 Value::String(s) => s.len(),
719 Value::Array(a) => a.len(),
720 _ => 0,
721 };
722 len < min as usize
723 } else {
724 false
725 }
726 }
727 "maxLength" => {
728 if is_empty {
729 false
730 } else if let Some(max) = rule_active.as_u64() {
731 let len = match field_data {
732 Value::String(s) => s.len(),
733 Value::Array(a) => a.len(),
734 _ => 0,
735 };
736 len > max as usize
737 } else {
738 false
739 }
740 }
741 "minValue" => {
742 if is_empty {
743 false
744 } else if let Some(min) = rule_active.as_f64() {
745 coerce_num(field_data).map_or(false, |v| v < min)
746 } else {
747 false
748 }
749 }
750 "maxValue" => {
751 if is_empty {
752 false
753 } else if let Some(max) = rule_active.as_f64() {
754 coerce_num(field_data).map_or(false, |v| v > max)
755 } else {
756 false
757 }
758 }
759 "pattern" | "evaluation" => false,
761 _ => {
762 if is_empty {
764 false
765 } else {
766 matches!(rule_active, Value::Bool(false) | Value::Null)
767 || rule_active.as_f64() == Some(0.0)
768 || rule_active.as_str().map_or(false, |s| s.is_empty())
769 || rule_active.as_array().map_or(false, |a| a.is_empty())
770 }
771 }
772 }
773}