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}