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