Skip to main content

json_eval_rs/jsoneval/
validation_cache.rs

1use crate::jsoneval::types::{ValidationError, ValidationResult};
2use indexmap::IndexMap;
3use serde_json::Value;
4
5/// Cached validation result for a single field
6#[derive(Clone, Debug)]
7pub struct CachedFieldValidation {
8    /// Field value at last validation
9    pub field_data: Value,
10    /// Visibility state at last validation
11    pub is_hidden: bool,
12    /// Whether readonly fields were validated
13    pub validate_readonly: bool,
14    /// Rule evaluation snapshot (to detect changes in dynamic $evaluation rules)
15    pub rules_snapshot: Value,
16    /// Cached error if invalid, None if valid
17    pub error: Option<ValidationError>,
18}
19
20/// Cache for validation results (whole-form and per-field)
21#[derive(Clone, Default, Debug)]
22pub struct ValidationCache {
23    /// Last input data string for whole-form caching
24    pub last_data_str: Option<String>,
25    /// Last context string for whole-form caching
26    pub last_context_str: Option<String>,
27    /// Whether last whole-form validation validated readonly fields
28    pub last_validate_readonly: bool,
29    /// Whether last whole-form validation included subforms
30    pub last_include_subforms: bool,
31    /// Last full validation result
32    pub last_result: Option<ValidationResult>,
33    /// Per-field validation cache
34    pub field_cache: IndexMap<String, CachedFieldValidation>,
35}
36
37impl ValidationCache {
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Check if the full validation result can be returned immediately
43    #[inline]
44    pub fn get_cached_full_result(
45        &self,
46        data: &str,
47        context: Option<&str>,
48        validate_readonly: bool,
49        include_subforms: bool,
50    ) -> Option<ValidationResult> {
51        if self.last_validate_readonly != validate_readonly
52            || self.last_include_subforms != include_subforms
53        {
54            return None;
55        }
56
57        let last_data = self.last_data_str.as_deref()?;
58        if last_data != data {
59            return None;
60        }
61
62        let ctx_matches = match (self.last_context_str.as_deref(), context) {
63            (None, None) => true,
64            (Some(""), None) | (None, Some("")) => true,
65            (Some("{}"), None) | (None, Some("{}")) => true,
66            (Some(a), Some(b)) => a == b,
67            _ => false,
68        };
69
70        if ctx_matches {
71            self.last_result.clone()
72        } else {
73            None
74        }
75    }
76
77    /// Check if a single field has a valid cache entry matching current state
78    #[inline]
79    pub fn check_field_cache(
80        &self,
81        field_path: &str,
82        field_data: &Value,
83        is_hidden: bool,
84        validate_readonly: bool,
85        rules: &Value,
86    ) -> Option<Option<ValidationError>> {
87        let cached = self.field_cache.get(field_path)?;
88        if cached.is_hidden == is_hidden
89            && cached.validate_readonly == validate_readonly
90            && &cached.field_data == field_data
91            && &cached.rules_snapshot == rules
92        {
93            Some(cached.error.clone())
94        } else {
95            None
96        }
97    }
98
99    /// Update cache for a single field
100    #[inline]
101    pub fn update_field(
102        &mut self,
103        field_path: String,
104        field_data: Value,
105        is_hidden: bool,
106        validate_readonly: bool,
107        rules_snapshot: Value,
108        error: Option<ValidationError>,
109    ) {
110        self.field_cache.insert(
111            field_path,
112            CachedFieldValidation {
113                field_data,
114                is_hidden,
115                validate_readonly,
116                rules_snapshot,
117                error,
118            },
119        );
120    }
121
122    /// Save full validation result
123    #[inline]
124    pub fn save_full_result(
125        &mut self,
126        data_str: String,
127        context_str: Option<String>,
128        validate_readonly: bool,
129        include_subforms: bool,
130        result: ValidationResult,
131    ) {
132        self.last_data_str = Some(data_str);
133        self.last_context_str = context_str;
134        self.last_validate_readonly = validate_readonly;
135        self.last_include_subforms = include_subforms;
136        self.last_result = Some(result);
137    }
138
139    /// Invalidate whole-result cache (e.g. on partial path validation)
140    #[inline]
141    pub fn invalidate_full_result(&mut self) {
142        self.last_data_str = None;
143        self.last_context_str = None;
144        self.last_validate_readonly = false;
145        self.last_include_subforms = false;
146        self.last_result = None;
147    }
148
149    /// Clear all cached validation state
150    #[inline]
151    pub fn clear(&mut self) {
152        self.last_data_str = None;
153        self.last_context_str = None;
154        self.last_validate_readonly = false;
155        self.last_include_subforms = false;
156        self.last_result = None;
157        self.field_cache.clear();
158    }
159}