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