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