json_eval_rs/jsoneval/validation.rs
1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::jsoneval::types::{ValidationError, ValidationResult};
5
6use crate::time_block;
7
8use indexmap::IndexMap;
9use serde_json::Value;
10
11
12impl JSONEval {
13 /// Validate data against schema rules
14 pub fn validate(
15 &mut self,
16 data: &str,
17 context: Option<&str>,
18 paths: Option<&[String]>,
19 ) -> Result<ValidationResult, String> {
20 time_block!("validate() [total]", {
21 // context is currently unused in validate implementation but kept for API compatibility?
22 // Or should we update self.context?
23 if let Some(ctx) = context {
24 // Parse context if needed?
25 // But validation primarily checks data.
26 // If rules depend on context (via $evaluation), we need to update context.
27 // Original validate implementation in lib.rs?
28 // I will assume simple implementation for now: parse data as before.
29 // But handle context update if provided?
30 // For safety/compat, let's parse context if provided.
31 if let Ok(ctx_val) = json_parser::parse_json_str(ctx) {
32 self.context = ctx_val;
33 // Purge cache for context change will be handled?
34 // My previous implementation only handled data change.
35 }
36 }
37
38 // Update data if provided
39 let data_value = {
40 let val = json_parser::parse_json_str(data)?;
41 self.data = val.clone();
42 // Update eval_data as well?
43 // Ideally yes, validation runs on current state.
44 // But validate usually assumes evaluate has been called or data is up to date?
45 val
46 };
47
48 // Acquire lock for evaluating dependent rules
49 let _lock = self.eval_lock.lock().unwrap();
50
51 // We need to ensure rules are evaluated against THIS data.
52 // If data changed, we need to update cache/eval_data logic.
53 // Since data is passed as reference, we always check.
54 {
55 let old_data = self.eval_data.clone_data_without(&["$params"]);
56 self.eval_data.replace_data_and_context(data_value.clone(), self.context.clone());
57
58 // Purge cache
59 // Selectively purge cache for rule evaluations that depend on changed data
60 // Collect all top-level data keys as potentially changed paths
61 let changed_data_paths: Vec<String> = if let Some(obj) = data_value.as_object() {
62 obj.keys().map(|k| format!("/{}", k)).collect()
63 } else {
64 Vec::new()
65 };
66 self.purge_cache_for_changed_data_with_comparison(
67 &changed_data_paths,
68 &old_data,
69 &data_value,
70 );
71 }
72
73 // Drop lock before calling evaluate_others which needs mutable access
74 drop(_lock);
75
76 // Re-evaluate rule evaluations to ensure fresh values
77 // This ensures all rule.$evaluation expressions are re-computed
78 self.evaluate_others(paths);
79
80 // Update evaluated_schema with fresh evaluations
81 self.evaluated_schema = self.get_evaluated_schema(false);
82
83 let mut errors: IndexMap<String, ValidationError> = IndexMap::new();
84
85 // Use pre-parsed fields_with_rules from schema parsing (no runtime collection needed)
86 // This list was collected during schema parse and contains all fields with rules
87 for field_path in self.fields_with_rules.iter() {
88 // Check if we should validate this path (path filtering)
89 if let Some(filter_paths) = paths {
90 if !filter_paths.is_empty()
91 && !filter_paths.iter().any(|p| {
92 field_path.starts_with(p.as_str()) || p.starts_with(field_path.as_str())
93 })
94 {
95 continue;
96 }
97 }
98
99 self.validate_field(field_path, &data_value, &mut errors);
100 }
101
102 let has_error = !errors.is_empty();
103
104 Ok(ValidationResult { has_error, errors })
105 })
106 }
107
108 /// Validate a single field that has rules
109 pub(crate) fn validate_field(
110 &self,
111 field_path: &str,
112 data: &Value,
113 errors: &mut IndexMap<String, ValidationError>,
114 ) {
115 // Skip if already has error
116 if errors.contains_key(field_path) {
117 return;
118 }
119
120 // Get schema for this field
121 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
122
123 // Remove leading "#" from path for pointer lookup
124 let pointer_path = schema_path.trim_start_matches('#');
125
126 // Try to get schema, if not found, try with /properties/ prefix for standard JSON Schema
127 let field_schema = match self.evaluated_schema.pointer(pointer_path) {
128 Some(s) => s,
129 None => {
130 // Try with /properties/ prefix (for standard JSON Schema format)
131 let alt_path = format!("/properties{}", pointer_path);
132 match self.evaluated_schema.pointer(&alt_path) {
133 Some(s) => s,
134 None => return,
135 }
136 }
137 };
138
139 // Check if field is hidden (skip validation)
140 if let Value::Object(schema_map) = field_schema {
141 if let Some(Value::Object(condition)) = schema_map.get("condition") {
142 if let Some(Value::Bool(true)) = condition.get("hidden") {
143 return;
144 }
145 }
146
147 // Get rules object
148 let rules = match schema_map.get("rules") {
149 Some(Value::Object(r)) => r,
150 _ => return,
151 };
152
153 // Get field data
154 let field_data = self.get_field_data(field_path, data);
155
156 // Validate each rule
157 for (rule_name, rule_value) in rules {
158 self.validate_rule(
159 field_path,
160 rule_name,
161 rule_value,
162 &field_data,
163 schema_map,
164 field_schema,
165 errors,
166 );
167 }
168 }
169 }
170
171 /// Get data value for a field path
172 pub(crate) fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
173 let parts: Vec<&str> = field_path.split('.').collect();
174 let mut current = data;
175
176 for part in parts {
177 match current {
178 Value::Object(map) => {
179 current = map.get(part).unwrap_or(&Value::Null);
180 }
181 _ => return Value::Null,
182 }
183 }
184
185 current.clone()
186 }
187
188 /// Validate a single rule
189 #[allow(clippy::too_many_arguments)]
190 pub(crate) fn validate_rule(
191 &self,
192 field_path: &str,
193 rule_name: &str,
194 rule_value: &Value,
195 field_data: &Value,
196 schema_map: &serde_json::Map<String, Value>,
197 _schema: &Value,
198 errors: &mut IndexMap<String, ValidationError>,
199 ) {
200 // Skip if already has error
201 if errors.contains_key(field_path) {
202 return;
203 }
204
205 let mut disabled_field = false;
206 // Check if disabled
207 if let Some(Value::Object(condition)) = schema_map.get("condition") {
208 if let Some(Value::Bool(true)) = condition.get("disabled") {
209 disabled_field = true;
210 }
211 }
212
213 // Get the evaluated rule from evaluated_schema (which has $evaluation already processed)
214 // Convert field_path to schema path
215 let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
216 let rule_path = format!(
217 "{}/rules/{}",
218 schema_path.trim_start_matches('#'),
219 rule_name
220 );
221
222 // Look up the evaluated rule from evaluated_schema
223 let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
224 eval_rule.clone()
225 } else {
226 rule_value.clone()
227 };
228
229 // Extract rule active status, message, etc
230 // Logic depends on rule structure (object with value/message or direct value)
231
232 let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
233 Value::Object(rule_obj) => {
234 let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
235
236 // Handle message - could be string or object with "value"
237 let message = match rule_obj.get("message") {
238 Some(Value::String(s)) => s.clone(),
239 Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => msg_obj
240 .get("value")
241 .and_then(|v| v.as_str())
242 .unwrap_or("Validation failed")
243 .to_string(),
244 Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
245 None => "Validation failed".to_string(),
246 };
247
248 let code = rule_obj
249 .get("code")
250 .and_then(|c| c.as_str())
251 .map(|s| s.to_string());
252
253 // Handle data - extract "value" from objects with $evaluation
254 let data = rule_obj.get("data").map(|d| {
255 if let Value::Object(data_obj) = d {
256 let mut cleaned_data = serde_json::Map::new();
257 for (key, value) in data_obj {
258 // If value is an object with only "value" key, extract it
259 if let Value::Object(val_obj) = value {
260 if val_obj.len() == 1 && val_obj.contains_key("value") {
261 cleaned_data.insert(key.clone(), val_obj["value"].clone());
262 } else {
263 cleaned_data.insert(key.clone(), value.clone());
264 }
265 } else {
266 cleaned_data.insert(key.clone(), value.clone());
267 }
268 }
269 Value::Object(cleaned_data)
270 } else {
271 d.clone()
272 }
273 });
274
275 (active.clone(), message, code, data)
276 }
277 _ => (
278 evaluated_rule.clone(),
279 "Validation failed".to_string(),
280 None,
281 None,
282 ),
283 };
284
285 // Generate default code if not provided
286 let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
287
288 let is_empty = matches!(field_data, Value::Null)
289 || (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty())
290 || (field_data.is_array() && field_data.as_array().unwrap().is_empty());
291
292 match rule_name {
293 "required" => {
294 if !disabled_field && rule_active == Value::Bool(true) {
295 if is_empty {
296 errors.insert(
297 field_path.to_string(),
298 ValidationError {
299 rule_type: "required".to_string(),
300 message: rule_message,
301 code: error_code.clone(),
302 pattern: None,
303 field_value: None,
304 data: None,
305 },
306 );
307 }
308 }
309 }
310 "minLength" => {
311 if !is_empty {
312 if let Some(min) = rule_active.as_u64() {
313 let len = match field_data {
314 Value::String(s) => s.len(),
315 Value::Array(a) => a.len(),
316 _ => 0,
317 };
318 if len < min as usize {
319 errors.insert(
320 field_path.to_string(),
321 ValidationError {
322 rule_type: "minLength".to_string(),
323 message: rule_message,
324 code: error_code.clone(),
325 pattern: None,
326 field_value: None,
327 data: None,
328 },
329 );
330 }
331 }
332 }
333 }
334 "maxLength" => {
335 if !is_empty {
336 if let Some(max) = rule_active.as_u64() {
337 let len = match field_data {
338 Value::String(s) => s.len(),
339 Value::Array(a) => a.len(),
340 _ => 0,
341 };
342 if len > max as usize {
343 errors.insert(
344 field_path.to_string(),
345 ValidationError {
346 rule_type: "maxLength".to_string(),
347 message: rule_message,
348 code: error_code.clone(),
349 pattern: None,
350 field_value: None,
351 data: None,
352 },
353 );
354 }
355 }
356 }
357 }
358 "minValue" => {
359 if !is_empty {
360 if let Some(min) = rule_active.as_f64() {
361 if let Some(val) = field_data.as_f64() {
362 if val < min {
363 errors.insert(
364 field_path.to_string(),
365 ValidationError {
366 rule_type: "minValue".to_string(),
367 message: rule_message,
368 code: error_code.clone(),
369 pattern: None,
370 field_value: None,
371 data: None,
372 },
373 );
374 }
375 }
376 }
377 }
378 }
379 "maxValue" => {
380 if !is_empty {
381 if let Some(max) = rule_active.as_f64() {
382 if let Some(val) = field_data.as_f64() {
383 if val > max {
384 errors.insert(
385 field_path.to_string(),
386 ValidationError {
387 rule_type: "maxValue".to_string(),
388 message: rule_message,
389 code: error_code.clone(),
390 pattern: None,
391 field_value: None,
392 data: None,
393 },
394 );
395 }
396 }
397 }
398 }
399 }
400 "pattern" => {
401 if !is_empty {
402 if let Some(pattern) = rule_active.as_str() {
403 if let Some(text) = field_data.as_str() {
404 if let Ok(regex) = regex::Regex::new(pattern) {
405 if !regex.is_match(text) {
406 errors.insert(
407 field_path.to_string(),
408 ValidationError {
409 rule_type: "pattern".to_string(),
410 message: rule_message,
411 code: error_code.clone(),
412 pattern: Some(pattern.to_string()),
413 field_value: Some(text.to_string()),
414 data: None,
415 },
416 );
417 }
418 }
419 }
420 }
421 }
422 }
423 "evaluation" => {
424 // Handle array of evaluation rules
425 // Format: "evaluation": [{ "code": "...", "message": "...", "$evaluation": {...} }]
426 if let Value::Array(eval_array) = &evaluated_rule {
427 for (idx, eval_item) in eval_array.iter().enumerate() {
428 if let Value::Object(eval_obj) = eval_item {
429 // Get the evaluated value (should be in "value" key after evaluation)
430 let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
431
432 // Check if result is falsy
433 let is_falsy = match eval_result {
434 Value::Bool(false) => true,
435 Value::Null => true,
436 Value::Number(n) => n.as_f64() == Some(0.0),
437 Value::String(s) => s.is_empty(),
438 Value::Array(a) => a.is_empty(),
439 _ => false,
440 };
441
442 if is_falsy {
443 let eval_code = eval_obj
444 .get("code")
445 .and_then(|c| c.as_str())
446 .map(|s| s.to_string())
447 .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
448
449 let eval_message = eval_obj
450 .get("message")
451 .and_then(|m| m.as_str())
452 .unwrap_or("Validation failed")
453 .to_string();
454
455 let eval_data = eval_obj.get("data").cloned();
456
457 errors.insert(
458 field_path.to_string(),
459 ValidationError {
460 rule_type: "evaluation".to_string(),
461 message: eval_message,
462 code: eval_code,
463 pattern: None,
464 field_value: None,
465 data: eval_data,
466 },
467 );
468
469 // Stop at first failure
470 break;
471 }
472 }
473 }
474 }
475 }
476 _ => {
477 // Custom evaluation rules
478 // In JS: if (!opt.rule.value) then error
479 // This handles rules with $evaluation that return false/falsy values
480 if !is_empty {
481 // Check if rule_active is falsy (false, 0, null, empty string, empty array)
482 let is_falsy = match &rule_active {
483 Value::Bool(false) => true,
484 Value::Null => true,
485 Value::Number(n) => n.as_f64() == Some(0.0),
486 Value::String(s) => s.is_empty(),
487 Value::Array(a) => a.is_empty(),
488 _ => false,
489 };
490
491 if is_falsy {
492 errors.insert(
493 field_path.to_string(),
494 ValidationError {
495 rule_type: "evaluation".to_string(),
496 message: rule_message,
497 code: error_code.clone(),
498 pattern: None,
499 field_value: None,
500 data: rule_data,
501 },
502 );
503 }
504 }
505 }
506 }
507 }
508}