Skip to main content

json_eval_rs/jsoneval/
validation.rs

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