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 fn validate(
15 &mut self,
16 data: &str,
17 context: Option<&str>,
18 paths: Option<&[String]>,
19 token: Option<&CancellationToken>,
20 ) -> Result<ValidationResult, String> {
21 if let Some(t) = token {
22 if t.is_cancelled() {
23 return Err("Cancelled".to_string());
24 }
25 }
26 time_block!("validate() [total]", {
27 let _lock = self.eval_lock.lock().unwrap();
29
30 let data_value = json_parser::parse_json_str(data)?;
32 let context_value = if let Some(ctx) = context {
33 json_parser::parse_json_str(ctx)?
34 } else {
35 Value::Object(serde_json::Map::new())
36 };
37
38 self.context = context_value.clone();
40
41 self.eval_data
43 .replace_data_and_context(data_value.clone(), context_value);
44
45 drop(_lock);
47
48 self.evaluate_others(paths, token);
51
52 self.evaluated_schema = self.get_evaluated_schema();
54
55 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
56
57 for field_path in self.fields_with_rules.iter() {
60 if let Some(filter_paths) = paths {
62 if !filter_paths.is_empty()
63 && !filter_paths.iter().any(|p| {
64 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
65 })
66 {
67 continue;
68 }
69 }
70
71 self.validate_field(field_path, &data_value, &mut errors);
72
73 if let Some(t) = token {
74 if t.is_cancelled() {
75 return Err("Cancelled".to_string());
76 }
77 }
78 }
79
80 let has_error = !errors.is_empty();
81
82 Ok(ValidationResult { has_error, errors })
83 })
84 }
85
86 pub(crate) fn validate_pre_set(
91 &mut self,
92 data_value: Value,
93 paths: Option<&[String]>,
94 token: Option<&CancellationToken>,
95 ) -> Result<crate::ValidationResult, String> {
96 self.evaluate_others(paths, token);
98 self.evaluated_schema = self.get_evaluated_schema();
99
100 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
101
102 let fields: Vec<String> = self.fields_with_rules.iter().cloned().collect();
103 for field_path in &fields {
104 if let Some(filter_paths) = paths {
105 if !filter_paths.is_empty()
106 && !filter_paths.iter().any(|p| {
107 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
108 })
109 {
110 continue;
111 }
112 }
113 if let Some(t) = token {
114 if t.is_cancelled() {
115 return Err("Cancelled".to_string());
116 }
117 }
118 self.validate_field(field_path, &data_value, &mut errors);
119 }
120
121 let has_error = !errors.is_empty();
122 Ok(crate::ValidationResult { has_error, errors })
123 }
124
125 pub(crate) fn validate_field(
127 &self,
128 field_path: &str,
129 data: &Value,
130 errors: &mut IndexMap<String, ValidationError>,
131 ) {
132 if errors.contains_key(field_path) {
134 return;
135 }
136
137 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
139 let pointer_path = schema_path.trim_start_matches('#');
140
141 let (field_schema, resolved_path) = match self.evaluated_schema.pointer(pointer_path) {
143 Some(s) => (s, pointer_path.to_string()),
144 None => {
145 let alt_path = format!("/properties{}", pointer_path);
146 match self.evaluated_schema.pointer(&alt_path) {
147 Some(s) => (s, alt_path),
148 None => return,
149 }
150 }
151 };
152
153 if self.is_effective_hidden(&resolved_path) {
155 return;
156 }
157
158 if let Value::Object(schema_map) = field_schema {
159 let rules = match schema_map.get("rules") {
161 Some(Value::Object(r)) => r,
162 _ => return,
163 };
164
165 let field_data = self.get_field_data(field_path, data);
167
168 for (rule_name, rule_value) in rules {
170 self.validate_rule(
171 field_path,
172 rule_name,
173 rule_value,
174 &field_data,
175 schema_map,
176 field_schema,
177 errors,
178 );
179 }
180 }
181 }
182
183 pub(crate) fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
185 let parts: Vec<&str> = field_path.split('.').collect();
186 let mut current = data;
187
188 for part in parts {
189 match current {
190 Value::Object(map) => {
191 current = map.get(part).unwrap_or(&Value::Null);
192 }
193 _ => return Value::Null,
194 }
195 }
196
197 current.clone()
198 }
199
200 #[allow(clippy::too_many_arguments)]
202 pub(crate) fn validate_rule(
203 &self,
204 field_path: &str,
205 rule_name: &str,
206 rule_value: &Value,
207 field_data: &Value,
208 schema_map: &serde_json::Map<String, Value>,
209 _schema: &Value,
210 errors: &mut IndexMap<String, ValidationError>,
211 ) {
212 if errors.contains_key(field_path) {
214 return;
215 }
216
217 let mut disabled_field = false;
218 if let Some(Value::Object(condition)) = schema_map.get("condition") {
220 if let Some(Value::Bool(true)) = condition.get("disabled") {
221 disabled_field = true;
222 }
223 }
224
225 let schema_type = schema_map
226 .get("type")
227 .and_then(|t| t.as_str())
228 .unwrap_or("");
229
230 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
233 let rule_path = format!(
234 "{}/rules/{}",
235 schema_path.trim_start_matches('#'),
236 rule_name
237 );
238
239 let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
241 eval_rule.clone()
242 } else {
243 rule_value.clone()
244 };
245
246 let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
250 Value::Object(rule_obj) => {
251 let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
252
253 let message = match rule_obj.get("message") {
255 Some(Value::String(s)) => s.clone(),
256 Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => msg_obj
257 .get("value")
258 .and_then(|v| v.as_str())
259 .unwrap_or("Validation failed")
260 .to_string(),
261 Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
262 None => "Validation failed".to_string(),
263 };
264
265 let code = rule_obj
266 .get("code")
267 .and_then(|c| c.as_str())
268 .map(|s| s.to_string());
269
270 let data = rule_obj.get("data").map(|d| {
272 if let Value::Object(data_obj) = d {
273 let mut cleaned_data = serde_json::Map::new();
274 for (key, value) in data_obj {
275 if let Value::Object(val_obj) = value {
277 if val_obj.len() == 1 && val_obj.contains_key("value") {
278 cleaned_data.insert(key.clone(), val_obj["value"].clone());
279 } else {
280 cleaned_data.insert(key.clone(), value.clone());
281 }
282 } else {
283 cleaned_data.insert(key.clone(), value.clone());
284 }
285 }
286 Value::Object(cleaned_data)
287 } else {
288 d.clone()
289 }
290 });
291
292 (active.clone(), message, code, data)
293 }
294 _ => (
295 evaluated_rule.clone(),
296 "Validation failed".to_string(),
297 None,
298 None,
299 ),
300 };
301
302 let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
304
305 let is_empty = matches!(field_data, Value::Null)
306 || (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty())
307 || (field_data.is_array() && field_data.as_array().unwrap().is_empty());
308
309 match rule_name {
310 "required" => {
311 if !disabled_field && rule_active == Value::Bool(true) {
312 if is_empty {
313 errors.insert(
314 field_path.to_string(),
315 ValidationError {
316 rule_type: "required".to_string(),
317 message: rule_message,
318 code: error_code.clone(),
319 pattern: None,
320 field_value: None,
321 data: None,
322 },
323 );
324 }
325 }
326 }
327 "minLength" | "maxLength" | "minValue" | "maxValue" => {
328 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
329 errors.insert(
330 field_path.to_string(),
331 ValidationError {
332 rule_type: rule_name.to_string(),
333 message: rule_message,
334 code: error_code.clone(),
335 pattern: None,
336 field_value: None,
337 data: None,
338 },
339 );
340 }
341 }
342
343 "pattern" => {
344 if !is_empty {
345 if let Some(pattern) = rule_active.as_str() {
346 if let Some(text) = field_data.as_str() {
347 let mut cache = self.regex_cache.write().unwrap();
348 let regex = cache.entry(pattern.to_string()).or_insert_with(|| {
349 regex::Regex::new(pattern)
350 .unwrap_or_else(|_| regex::Regex::new("(?:)").unwrap())
351 });
352 if !regex.is_match(text) {
353 errors.insert(
354 field_path.to_string(),
355 ValidationError {
356 rule_type: "pattern".to_string(),
357 message: rule_message,
358 code: error_code.clone(),
359 pattern: Some(pattern.to_string()),
360 field_value: Some(text.to_string()),
361 data: None,
362 },
363 );
364 }
365 }
366 }
367 }
368 }
369 "evaluation" => {
370 if let Value::Array(eval_array) = &evaluated_rule {
373 for (idx, eval_item) in eval_array.iter().enumerate() {
374 if let Value::Object(eval_obj) = eval_item {
375 let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
377
378 let is_falsy = match eval_result {
380 Value::Bool(false) => true,
381 Value::Null => true,
382 Value::Number(n) => n.as_f64() == Some(0.0),
383 Value::String(s) => s.is_empty(),
384 Value::Array(a) => a.is_empty(),
385 _ => false,
386 };
387
388 if is_falsy {
389 let eval_code = eval_obj
390 .get("code")
391 .and_then(|c| c.as_str())
392 .map(|s| s.to_string())
393 .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
394
395 let eval_message = eval_obj
396 .get("message")
397 .and_then(|m| m.as_str())
398 .unwrap_or("Validation failed")
399 .to_string();
400
401 let eval_data = eval_obj.get("data").cloned();
402
403 errors.insert(
404 field_path.to_string(),
405 ValidationError {
406 rule_type: "evaluation".to_string(),
407 message: eval_message,
408 code: eval_code,
409 pattern: None,
410 field_value: None,
411 data: eval_data,
412 },
413 );
414
415 break;
417 }
418 }
419 }
420 }
421 }
422 _ => {
423 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
424 errors.insert(
425 field_path.to_string(),
426 ValidationError {
427 rule_type: "evaluation".to_string(),
428 message: rule_message,
429 code: error_code.clone(),
430 pattern: None,
431 field_value: None,
432 data: rule_data,
433 },
434 );
435 }
436 }
437 }
438 }
439
440 pub(crate) fn dep_fails_schema_rules(
447 &self,
448 field_path: &str,
449 field_data: &Value,
450 scope_data: &Value,
451 ) -> bool {
452 let schema_pointer = path_utils::dot_notation_to_schema_pointer(field_path);
453 let pointer = schema_pointer.trim_start_matches('#');
454
455 let field_schema = match self.schema.pointer(pointer) {
456 Some(s) => s,
457 None => {
458 let alt_pointer = format!("/properties{}", pointer);
459 match self.schema.pointer(&alt_pointer) {
460 Some(s) => s,
461 None => return false,
462 }
463 }
464 };
465
466 let schema_map = match field_schema.as_object() {
467 Some(m) => m,
468 None => return false,
469 };
470
471 let rules = match schema_map.get("rules") {
472 Some(Value::Object(r)) => r,
473 _ => return false,
474 };
475
476 let schema_type = schema_map
477 .get("type")
478 .and_then(|t| t.as_str())
479 .unwrap_or("");
480
481 let is_empty = matches!(field_data, Value::Null)
482 || field_data.as_str().map_or(false, |s| s.is_empty())
483 || field_data.as_array().map_or(false, |a| a.is_empty());
484
485 for (rule_name, rule_value) in rules {
486 let rule_eval_key = format!("#{}/rules/{}", pointer, rule_name);
490 let rule_active: Value = if let Some(logic_id) = self.evaluations.get(&rule_eval_key) {
491 let empty_ctx = Value::Object(serde_json::Map::new());
492 self.engine
493 .run_with_context(logic_id, scope_data, &empty_ctx)
494 .unwrap_or(Value::Null)
495 } else {
496 match rule_value {
497 Value::Object(obj) => obj.get("value").cloned().unwrap_or(Value::Null),
498 other => other.clone(),
499 }
500 };
501
502 if rule_value_fails(rule_name, &rule_active, field_data, is_empty, schema_type) {
503 return true;
504 }
505 }
506
507 false
508 }
509}
510
511fn rule_value_fails(
520 rule_name: &str,
521 rule_active: &Value,
522 field_data: &Value,
523 is_empty: bool,
524 schema_type: &str,
525) -> bool {
526 let coerce_num = |v: &Value| -> Option<f64> {
527 if let Some(n) = v.as_f64() {
528 return Some(n);
529 }
530 if matches!(schema_type, "number" | "integer") {
531 if let Some(s) = v.as_str() {
532 return s.trim().parse::<f64>().ok();
533 }
534 }
535 None
536 };
537
538 match rule_name {
539 "required" => is_empty && matches!(rule_active, Value::Bool(true)),
540 "minLength" => {
541 if is_empty {
542 false
543 } else if let Some(min) = rule_active.as_u64() {
544 let len = match field_data {
545 Value::String(s) => s.len(),
546 Value::Array(a) => a.len(),
547 _ => 0,
548 };
549 len < min as usize
550 } else {
551 false
552 }
553 }
554 "maxLength" => {
555 if is_empty {
556 false
557 } else if let Some(max) = rule_active.as_u64() {
558 let len = match field_data {
559 Value::String(s) => s.len(),
560 Value::Array(a) => a.len(),
561 _ => 0,
562 };
563 len > max as usize
564 } else {
565 false
566 }
567 }
568 "minValue" => {
569 if is_empty {
570 false
571 } else if let Some(min) = rule_active.as_f64() {
572 coerce_num(field_data).map_or(false, |v| v < min)
573 } else {
574 false
575 }
576 }
577 "maxValue" => {
578 if is_empty {
579 false
580 } else if let Some(max) = rule_active.as_f64() {
581 coerce_num(field_data).map_or(false, |v| v > max)
582 } else {
583 false
584 }
585 }
586 "pattern" | "evaluation" => false,
588 _ => {
589 if is_empty {
591 false
592 } else {
593 matches!(rule_active, Value::Bool(false) | Value::Null)
594 || rule_active.as_f64() == Some(0.0)
595 || rule_active.as_str().map_or(false, |s| s.is_empty())
596 || rule_active.as_array().map_or(false, |a| a.is_empty())
597 }
598 }
599 }
600}