Skip to main content

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.snapshot_data();
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        // Resolve schema for this field
111        let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
112        let pointer_path = schema_path.trim_start_matches('#');
113
114        // Try to get schema, if not found, try with /properties/ prefix for standard JSON Schema
115        let (field_schema, resolved_path) = match self.evaluated_schema.pointer(pointer_path) {
116            Some(s) => (s, pointer_path.to_string()),
117            None => {
118                let alt_path = format!("/properties{}", pointer_path);
119                match self.evaluated_schema.pointer(&alt_path) {
120                    Some(s) => (s, alt_path),
121                    None => return,
122                }
123            }
124        };
125
126        // Skip hidden fields
127        if self.is_effective_hidden(&resolved_path) {
128            return;
129        }
130
131        if let Value::Object(schema_map) = field_schema {
132
133            // Get rules object
134            let rules = match schema_map.get("rules") {
135                Some(Value::Object(r)) => r,
136                _ => return,
137            };
138
139            // Get field data
140            let field_data = self.get_field_data(field_path, data);
141
142            // Validate each rule
143            for (rule_name, rule_value) in rules {
144                self.validate_rule(
145                    field_path,
146                    rule_name,
147                    rule_value,
148                    &field_data,
149                    schema_map,
150                    field_schema,
151                    errors,
152                );
153            }
154        }
155    }
156
157    /// Get data value for a field path
158    pub(crate) fn get_field_data(&self, field_path: &str, data: &Value) -> Value {
159        let parts: Vec<&str> = field_path.split('.').collect();
160        let mut current = data;
161
162        for part in parts {
163            match current {
164                Value::Object(map) => {
165                    current = map.get(part).unwrap_or(&Value::Null);
166                }
167                _ => return Value::Null,
168            }
169        }
170
171        current.clone()
172    }
173
174    /// Validate a single rule
175    #[allow(clippy::too_many_arguments)]
176    pub(crate) fn validate_rule(
177        &self,
178        field_path: &str,
179        rule_name: &str,
180        rule_value: &Value,
181        field_data: &Value,
182        schema_map: &serde_json::Map<String, Value>,
183        _schema: &Value,
184        errors: &mut IndexMap<String, ValidationError>,
185    ) {
186        // Skip if already has error
187        if errors.contains_key(field_path) {
188            return;
189        }
190
191        let mut disabled_field = false;
192        // Check if disabled
193        if let Some(Value::Object(condition)) = schema_map.get("condition") {
194            if let Some(Value::Bool(true)) = condition.get("disabled") {
195                disabled_field = true;
196            }
197        }
198
199        // Get the evaluated rule from evaluated_schema (which has $evaluation already processed)
200        // Convert field_path to schema path
201        let schema_path = path_utils::dot_notation_to_schema_pointer(field_path);
202        let rule_path = format!(
203            "{}/rules/{}",
204            schema_path.trim_start_matches('#'),
205            rule_name
206        );
207
208        // Look up the evaluated rule from evaluated_schema
209        let evaluated_rule = if let Some(eval_rule) = self.evaluated_schema.pointer(&rule_path) {
210            eval_rule.clone()
211        } else {
212            rule_value.clone()
213        };
214
215        // Extract rule active status, message, etc
216        // Logic depends on rule structure (object with value/message or direct value)
217        
218        let (rule_active, rule_message, rule_code, rule_data) = match &evaluated_rule {
219            Value::Object(rule_obj) => {
220                let active = rule_obj.get("value").unwrap_or(&Value::Bool(false));
221
222                // Handle message - could be string or object with "value"
223                let message = match rule_obj.get("message") {
224                    Some(Value::String(s)) => s.clone(),
225                    Some(Value::Object(msg_obj)) if msg_obj.contains_key("value") => msg_obj
226                        .get("value")
227                        .and_then(|v| v.as_str())
228                        .unwrap_or("Validation failed")
229                        .to_string(),
230                    Some(msg_val) => msg_val.as_str().unwrap_or("Validation failed").to_string(),
231                    None => "Validation failed".to_string(),
232                };
233
234                let code = rule_obj
235                    .get("code")
236                    .and_then(|c| c.as_str())
237                    .map(|s| s.to_string());
238
239                // Handle data - extract "value" from objects with $evaluation
240                let data = rule_obj.get("data").map(|d| {
241                    if let Value::Object(data_obj) = d {
242                        let mut cleaned_data = serde_json::Map::new();
243                        for (key, value) in data_obj {
244                            // If value is an object with only "value" key, extract it
245                            if let Value::Object(val_obj) = value {
246                                if val_obj.len() == 1 && val_obj.contains_key("value") {
247                                    cleaned_data.insert(key.clone(), val_obj["value"].clone());
248                                } else {
249                                    cleaned_data.insert(key.clone(), value.clone());
250                                }
251                            } else {
252                                cleaned_data.insert(key.clone(), value.clone());
253                            }
254                        }
255                        Value::Object(cleaned_data)
256                    } else {
257                        d.clone()
258                    }
259                });
260
261                (active.clone(), message, code, data)
262            }
263            _ => (
264                evaluated_rule.clone(),
265                "Validation failed".to_string(),
266                None,
267                None,
268            ),
269        };
270
271        // Generate default code if not provided
272        let error_code = rule_code.or_else(|| Some(format!("{}.{}", field_path, rule_name)));
273
274        let is_empty = matches!(field_data, Value::Null)
275            || (field_data.is_string() && field_data.as_str().unwrap_or("").is_empty())
276            || (field_data.is_array() && field_data.as_array().unwrap().is_empty());
277
278        match rule_name {
279            "required" => {
280                if !disabled_field && rule_active == Value::Bool(true) {
281                    if is_empty {
282                        errors.insert(
283                            field_path.to_string(),
284                            ValidationError {
285                                rule_type: "required".to_string(),
286                                message: rule_message,
287                                code: error_code.clone(),
288                                pattern: None,
289                                field_value: None,
290                                data: None,
291                            },
292                        );
293                    }
294                }
295            }
296            "minLength" => {
297                if !is_empty {
298                    if let Some(min) = rule_active.as_u64() {
299                        let len = match field_data {
300                            Value::String(s) => s.len(),
301                            Value::Array(a) => a.len(),
302                            _ => 0,
303                        };
304                        if len < min as usize {
305                            errors.insert(
306                                field_path.to_string(),
307                                ValidationError {
308                                    rule_type: "minLength".to_string(),
309                                    message: rule_message,
310                                    code: error_code.clone(),
311                                    pattern: None,
312                                    field_value: None,
313                                    data: None,
314                                },
315                            );
316                        }
317                    }
318                }
319            }
320            "maxLength" => {
321                if !is_empty {
322                    if let Some(max) = rule_active.as_u64() {
323                        let len = match field_data {
324                            Value::String(s) => s.len(),
325                            Value::Array(a) => a.len(),
326                            _ => 0,
327                        };
328                        if len > max as usize {
329                            errors.insert(
330                                field_path.to_string(),
331                                ValidationError {
332                                    rule_type: "maxLength".to_string(),
333                                    message: rule_message,
334                                    code: error_code.clone(),
335                                    pattern: None,
336                                    field_value: None,
337                                    data: None,
338                                },
339                            );
340                        }
341                    }
342                }
343            }
344            "minValue" => {
345                if !is_empty {
346                    if let Some(min) = rule_active.as_f64() {
347                        if let Some(val) = field_data.as_f64() {
348                            if val < min {
349                                errors.insert(
350                                    field_path.to_string(),
351                                    ValidationError {
352                                        rule_type: "minValue".to_string(),
353                                        message: rule_message,
354                                        code: error_code.clone(),
355                                        pattern: None,
356                                        field_value: None,
357                                        data: None,
358                                    },
359                                );
360                            }
361                        }
362                    }
363                }
364            }
365            "maxValue" => {
366                if !is_empty {
367                    if let Some(max) = rule_active.as_f64() {
368                        if let Some(val) = field_data.as_f64() {
369                            if val > max {
370                                errors.insert(
371                                    field_path.to_string(),
372                                    ValidationError {
373                                        rule_type: "maxValue".to_string(),
374                                        message: rule_message,
375                                        code: error_code.clone(),
376                                        pattern: None,
377                                        field_value: None,
378                                        data: None,
379                                    },
380                                );
381                            }
382                        }
383                    }
384                }
385            }
386            "pattern" => {
387                if !is_empty {
388                    if let Some(pattern) = rule_active.as_str() {
389                        if let Some(text) = field_data.as_str() {
390                            let mut cache = self.regex_cache.write().unwrap();
391                            let regex = cache.entry(pattern.to_string()).or_insert_with(|| {
392                                regex::Regex::new(pattern).unwrap_or_else(|_| regex::Regex::new("(?:)").unwrap())
393                            });
394                            if !regex.is_match(text) {
395                                errors.insert(
396                                    field_path.to_string(),
397                                    ValidationError {
398                                        rule_type: "pattern".to_string(),
399                                        message: rule_message,
400                                        code: error_code.clone(),
401                                        pattern: Some(pattern.to_string()),
402                                        field_value: Some(text.to_string()),
403                                        data: None,
404                                    },
405                                );
406                            }
407                        }
408                    }
409                }
410            }
411            "evaluation" => {
412                // Handle array of evaluation rules
413                // Format: "evaluation": [{ "code": "...", "message": "...", "$evaluation": {...} }]
414                if let Value::Array(eval_array) = &evaluated_rule {
415                    for (idx, eval_item) in eval_array.iter().enumerate() {
416                        if let Value::Object(eval_obj) = eval_item {
417                            // Get the evaluated value (should be in "value" key after evaluation)
418                            let eval_result = eval_obj.get("value").unwrap_or(&Value::Bool(true));
419
420                            // Check if result is falsy
421                            let is_falsy = match eval_result {
422                                Value::Bool(false) => true,
423                                Value::Null => true,
424                                Value::Number(n) => n.as_f64() == Some(0.0),
425                                Value::String(s) => s.is_empty(),
426                                Value::Array(a) => a.is_empty(),
427                                _ => false,
428                            };
429
430                            if is_falsy {
431                                let eval_code = eval_obj
432                                    .get("code")
433                                    .and_then(|c| c.as_str())
434                                    .map(|s| s.to_string())
435                                    .or_else(|| Some(format!("{}.evaluation.{}", field_path, idx)));
436
437                                let eval_message = eval_obj
438                                    .get("message")
439                                    .and_then(|m| m.as_str())
440                                    .unwrap_or("Validation failed")
441                                    .to_string();
442
443                                let eval_data = eval_obj.get("data").cloned();
444
445                                errors.insert(
446                                    field_path.to_string(),
447                                    ValidationError {
448                                        rule_type: "evaluation".to_string(),
449                                        message: eval_message,
450                                        code: eval_code,
451                                        pattern: None,
452                                        field_value: None,
453                                        data: eval_data,
454                                    },
455                                );
456
457                                // Stop at first failure
458                                break;
459                            }
460                        }
461                    }
462                }
463            }
464            _ => {
465                // Custom evaluation rules
466                // In JS: if (!opt.rule.value) then error
467                // This handles rules with $evaluation that return false/falsy values
468                if !is_empty {
469                    // Check if rule_active is falsy (false, 0, null, empty string, empty array)
470                    let is_falsy = match &rule_active {
471                        Value::Bool(false) => true,
472                        Value::Null => true,
473                        Value::Number(n) => n.as_f64() == Some(0.0),
474                        Value::String(s) => s.is_empty(),
475                        Value::Array(a) => a.is_empty(),
476                        _ => false,
477                    };
478
479                    if is_falsy {
480                        errors.insert(
481                            field_path.to_string(),
482                            ValidationError {
483                                rule_type: "evaluation".to_string(),
484                                message: rule_message,
485                                code: error_code.clone(),
486                                pattern: None,
487                                field_value: None,
488                                data: rule_data,
489                            },
490                        );
491                    }
492                }
493            }
494        }
495    }
496}