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