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