1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::jsoneval::types::{ValidationError, ValidationResult};
5use crate::jsoneval::cancellation::CancellationToken;
6
7use crate::time_block;
8
9use indexmap::IndexMap;
10use serde_json::Value;
11
12
13impl JSONEval {
14 pub fn validate(
16 &mut self,
17 data: &str,
18 context: Option<&str>,
19 paths: Option<&[String]>,
20 token: Option<&CancellationToken>,
21 ) -> Result<ValidationResult, String> {
22 if let Some(t) = token {
23 if t.is_cancelled() {
24 return Err("Cancelled".to_string());
25 }
26 }
27 time_block!("validate() [total]", {
28 let _lock = self.eval_lock.lock().unwrap();
30
31 let old_data = self.eval_data.snapshot_data();
33 let old_context = self.context.clone();
34
35 let data_value = json_parser::parse_json_str(data)?;
37 let context_value = if let Some(ctx) = context {
38 json_parser::parse_json_str(ctx)?
39 } else {
40 Value::Object(serde_json::Map::new())
41 };
42
43 self.context = context_value.clone();
45
46 self.eval_data.replace_data_and_context(data_value.clone(), context_value);
48
49 self.purge_cache_for_changed_data_with_comparison(&old_data, &data_value);
54
55 if context.is_some() && old_context != self.context {
56 self.purge_cache_for_context_change();
57 }
58
59 drop(_lock);
61
62 let missed_keys = dashmap::DashSet::new();
65 self.evaluate_others(paths, token, &missed_keys);
66
67 self.evaluated_schema = self.get_evaluated_schema(false);
69
70 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
71
72 for field_path in self.fields_with_rules.iter() {
75 if let Some(filter_paths) = paths {
77 if !filter_paths.is_empty()
78 && !filter_paths.iter().any(|p| {
79 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
80 })
81 {
82 continue;
83 }
84 }
85
86 self.validate_field(field_path, &data_value, &mut errors);
87
88 if let Some(t) = token {
89 if t.is_cancelled() {
90 return Err("Cancelled".to_string());
91 }
92 }
93 }
94
95 let has_error = !errors.is_empty();
96
97 Ok(ValidationResult { has_error, errors })
98 })
99 }
100
101 pub(crate) fn validate_field(
103 &self,
104 field_path: &str,
105 data: &Value,
106 errors: &mut IndexMap<String, ValidationError>,
107 ) {
108 if errors.contains_key(field_path) {
110 return;
111 }
112
113 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
115 let pointer_path = schema_path.trim_start_matches('#');
116
117 let (field_schema, resolved_path) = match self.evaluated_schema.pointer(pointer_path) {
119 Some(s) => (s, pointer_path.to_string()),
120 None => {
121 let alt_path = format!("/properties{}", pointer_path);
122 match self.evaluated_schema.pointer(&alt_path) {
123 Some(s) => (s, alt_path),
124 None => return,
125 }
126 }
127 };
128
129 if self.is_effective_hidden(&resolved_path) {
131 return;
132 }
133
134 if let Value::Object(schema_map) = field_schema {
135
136 let rules = match schema_map.get("rules") {
138 Some(Value::Object(r)) => r,
139 _ => return,
140 };
141
142 let field_data = self.get_field_data(field_path, data);
144
145 for (rule_name, rule_value) in rules {
147 self.validate_rule(
148 field_path,
149 rule_name,
150 rule_value,
151 &field_data,
152 schema_map,
153 field_schema,
154 errors,
155 );
156 }
157 }
158 }
159
160 pub(crate) fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
162 let parts: Vec<&str> = field_path.split('.').collect();
163 let mut current = data;
164
165 for part in parts {
166 match current {
167 Value::Object(map) => {
168 current = map.get(part).unwrap_or(&Value::Null);
169 }
170 _ => return Value::Null,
171 }
172 }
173
174 current.clone()
175 }
176
177 #[allow(clippy::too_many_arguments)]
179 pub(crate) fn validate_rule(
180 &self,
181 field_path: &str,
182 rule_name: &str,
183 rule_value: &Value,
184 field_data: &Value,
185 schema_map: &serde_json::Map<String, Value>,
186 _schema: &Value,
187 errors: &mut IndexMap<String, ValidationError>,
188 ) {
189 if errors.contains_key(field_path) {
191 return;
192 }
193
194 let mut disabled_field = false;
195 if let Some(Value::Object(condition)) = schema_map.get("condition") {
197 if let Some(Value::Bool(true)) = condition.get("disabled") {
198 disabled_field = true;
199 }
200 }
201
202 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
205 let rule_path = format!(
206 "{}/rules/{}",
207 schema_path.trim_start_matches('#'),
208 rule_name
209 );
210
211 let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
213 eval_rule.clone()
214 } else {
215 rule_value.clone()
216 };
217
218 let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
222 Value::Object(rule_obj) => {
223 let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
224
225 let message = match rule_obj.get("message") {
227 Some(Value::String(s)) => s.clone(),
228 Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => msg_obj
229 .get("value")
230 .and_then(|v| v.as_str())
231 .unwrap_or("Validation failed")
232 .to_string(),
233 Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
234 None => "Validation failed".to_string(),
235 };
236
237 let code = rule_obj
238 .get("code")
239 .and_then(|c| c.as_str())
240 .map(|s| s.to_string());
241
242 let data = rule_obj.get("data").map(|d| {
244 if let Value::Object(data_obj) = d {
245 let mut cleaned_data = serde_json::Map::new();
246 for (key, value) in data_obj {
247 if let Value::Object(val_obj) = value {
249 if val_obj.len() == 1 && val_obj.contains_key("value") {
250 cleaned_data.insert(key.clone(), val_obj["value"].clone());
251 } else {
252 cleaned_data.insert(key.clone(), value.clone());
253 }
254 } else {
255 cleaned_data.insert(key.clone(), value.clone());
256 }
257 }
258 Value::Object(cleaned_data)
259 } else {
260 d.clone()
261 }
262 });
263
264 (active.clone(), message, code, data)
265 }
266 _ => (
267 evaluated_rule.clone(),
268 "Validation failed".to_string(),
269 None,
270 None,
271 ),
272 };
273
274 let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
276
277 let is_empty = matches!(field_data, Value::Null)
278 || (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty())
279 || (field_data.is_array() && field_data.as_array().unwrap().is_empty());
280
281 match rule_name {
282 "required" => {
283 if !disabled_field && rule_active == Value::Bool(true) {
284 if is_empty {
285 errors.insert(
286 field_path.to_string(),
287 ValidationError {
288 rule_type: "required".to_string(),
289 message: rule_message,
290 code: error_code.clone(),
291 pattern: None,
292 field_value: None,
293 data: None,
294 },
295 );
296 }
297 }
298 }
299 "minLength" => {
300 if !is_empty {
301 if let Some(min) = rule_active.as_u64() {
302 let len = match field_data {
303 Value::String(s) => s.len(),
304 Value::Array(a) => a.len(),
305 _ => 0,
306 };
307 if len < min as usize {
308 errors.insert(
309 field_path.to_string(),
310 ValidationError {
311 rule_type: "minLength".to_string(),
312 message: rule_message,
313 code: error_code.clone(),
314 pattern: None,
315 field_value: None,
316 data: None,
317 },
318 );
319 }
320 }
321 }
322 }
323 "maxLength" => {
324 if !is_empty {
325 if let Some(max) = rule_active.as_u64() {
326 let len = match field_data {
327 Value::String(s) => s.len(),
328 Value::Array(a) => a.len(),
329 _ => 0,
330 };
331 if len > max as usize {
332 errors.insert(
333 field_path.to_string(),
334 ValidationError {
335 rule_type: "maxLength".to_string(),
336 message: rule_message,
337 code: error_code.clone(),
338 pattern: None,
339 field_value: None,
340 data: None,
341 },
342 );
343 }
344 }
345 }
346 }
347 "minValue" => {
348 if !is_empty {
349 if let Some(min) = rule_active.as_f64() {
350 if let Some(val) = field_data.as_f64() {
351 if val < min {
352 errors.insert(
353 field_path.to_string(),
354 ValidationError {
355 rule_type: "minValue".to_string(),
356 message: rule_message,
357 code: error_code.clone(),
358 pattern: None,
359 field_value: None,
360 data: None,
361 },
362 );
363 }
364 }
365 }
366 }
367 }
368 "maxValue" => {
369 if !is_empty {
370 if let Some(max) = rule_active.as_f64() {
371 if let Some(val) = field_data.as_f64() {
372 if val > max {
373 errors.insert(
374 field_path.to_string(),
375 ValidationError {
376 rule_type: "maxValue".to_string(),
377 message: rule_message,
378 code: error_code.clone(),
379 pattern: None,
380 field_value: None,
381 data: None,
382 },
383 );
384 }
385 }
386 }
387 }
388 }
389 "pattern" => {
390 if !is_empty {
391 if let Some(pattern) = rule_active.as_str() {
392 if let Some(text) = field_data.as_str() {
393 let mut cache = self.regex_cache.write().unwrap();
394 let regex = cache.entry(pattern.to_string()).or_insert_with(|| {
395 regex::Regex::new(pattern).unwrap_or_else(|_| regex::Regex::new("(?:)").unwrap())
396 });
397 if !regex.is_match(text) {
398 errors.insert(
399 field_path.to_string(),
400 ValidationError {
401 rule_type: "pattern".to_string(),
402 message: rule_message,
403 code: error_code.clone(),
404 pattern: Some(pattern.to_string()),
405 field_value: Some(text.to_string()),
406 data: None,
407 },
408 );
409 }
410 }
411 }
412 }
413 }
414 "evaluation" => {
415 if let Value::Array(eval_array) = &evaluated_rule {
418 for (idx, eval_item) in eval_array.iter().enumerate() {
419 if let Value::Object(eval_obj) = eval_item {
420 let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
422
423 let is_falsy = match eval_result {
425 Value::Bool(false) => true,
426 Value::Null => true,
427 Value::Number(n) => n.as_f64() == Some(0.0),
428 Value::String(s) => s.is_empty(),
429 Value::Array(a) => a.is_empty(),
430 _ => false,
431 };
432
433 if is_falsy {
434 let eval_code = eval_obj
435 .get("code")
436 .and_then(|c| c.as_str())
437 .map(|s| s.to_string())
438 .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
439
440 let eval_message = eval_obj
441 .get("message")
442 .and_then(|m| m.as_str())
443 .unwrap_or("Validation failed")
444 .to_string();
445
446 let eval_data = eval_obj.get("data").cloned();
447
448 errors.insert(
449 field_path.to_string(),
450 ValidationError {
451 rule_type: "evaluation".to_string(),
452 message: eval_message,
453 code: eval_code,
454 pattern: None,
455 field_value: None,
456 data: eval_data,
457 },
458 );
459
460 break;
462 }
463 }
464 }
465 }
466 }
467 _ => {
468 if !is_empty {
472 let is_falsy = match &rule_active {
474 Value::Bool(false) => true,
475 Value::Null => true,
476 Value::Number(n) => n.as_f64() == Some(0.0),
477 Value::String(s) => s.is_empty(),
478 Value::Array(a) => a.is_empty(),
479 _ => false,
480 };
481
482 if is_falsy {
483 errors.insert(
484 field_path.to_string(),
485 ValidationError {
486 rule_type: "evaluation".to_string(),
487 message: rule_message,
488 code: error_code.clone(),
489 pattern: None,
490 field_value: None,
491 data: rule_data,
492 },
493 );
494 }
495 }
496 }
497 }
498 }
499}