Skip to main content

ai_agent/utils/settings/
settings_cache.rs

1// Source: ~/claudecode/openclaudecode/src/utils/settings/settingsCache.ts
2//! In-memory cache for settings data.
3//!
4//! Three caches:
5//! 1. Session settings cache (merged, validated settings)
6//! 2. Per-source cache (individual source settings)
7//! 3. Parse file cache (single file parse results)
8
9use std::collections::HashMap;
10use std::sync::{LazyLock, Mutex};
11
12use super::SettingSource;
13use super::validation::{SettingsWithErrors, ValidationError};
14
15/// Parsed settings from a single file.
16#[derive(Debug, Clone, Default, PartialEq)]
17pub struct ParsedSettings {
18    pub settings: Option<serde_json::Value>,
19    pub errors: Vec<ValidationError>,
20}
21
22struct SettingsCacheInner {
23    /// Merged session settings cache. Null = not yet computed.
24    session_settings: Option<SettingsWithErrors>,
25    /// Per-source cache. None = cache miss, Some(None) = cached null.
26    per_source_cache: HashMap<SettingSource, Option<serde_json::Value>>,
27    /// Path-keyed parse cache.
28    parse_file_cache: HashMap<String, ParsedSettings>,
29    /// Plugin settings base layer (lowest priority in cascade).
30    plugin_settings_base: Option<serde_json::Value>,
31}
32
33static SETTINGS_CACHE: LazyLock<Mutex<SettingsCacheInner>> = LazyLock::new(|| {
34    Mutex::new(SettingsCacheInner {
35        session_settings: None,
36        per_source_cache: HashMap::new(),
37        parse_file_cache: HashMap::new(),
38        plugin_settings_base: None,
39    })
40});
41
42/// Get the merged session settings cache.
43pub fn get_session_settings_cache() -> Option<SettingsWithErrors> {
44    SETTINGS_CACHE.lock().unwrap().session_settings.clone()
45}
46
47/// Set the merged session settings cache.
48pub fn set_session_settings_cache(value: SettingsWithErrors) {
49    SETTINGS_CACHE.lock().unwrap().session_settings = Some(value);
50}
51
52/// Get cached settings for a source.
53/// Returns `None` for cache miss, `Some(None)` for cached "no settings".
54pub fn get_cached_settings_for_source(
55    source: &SettingSource,
56) -> Option<Option<serde_json::Value>> {
57    SETTINGS_CACHE
58        .lock()
59        .unwrap()
60        .per_source_cache
61        .get(source)
62        .cloned()
63}
64
65/// Set cached settings for a source.
66pub fn set_cached_settings_for_source(
67    source: &SettingSource,
68    value: Option<serde_json::Value>,
69) {
70    let mut cache = SETTINGS_CACHE.lock().unwrap();
71    cache.per_source_cache.insert(source.clone(), value);
72}
73
74/// Get cached parsed file result.
75pub fn get_cached_parsed_file(path: &str) -> Option<ParsedSettings> {
76    SETTINGS_CACHE
77        .lock()
78        .unwrap()
79        .parse_file_cache
80        .get(path)
81        .cloned()
82}
83
84/// Set cached parsed file result.
85pub fn set_cached_parsed_file(path: &str, value: ParsedSettings) {
86    SETTINGS_CACHE
87        .lock()
88        .unwrap()
89        .parse_file_cache
90        .insert(path.to_string(), value);
91}
92
93/// Get the plugin settings base layer.
94pub fn get_plugin_settings_base() -> Option<serde_json::Value> {
95    SETTINGS_CACHE.lock().unwrap().plugin_settings_base.clone()
96}
97
98/// Set the plugin settings base layer.
99pub fn set_plugin_settings_base(value: Option<serde_json::Value>) {
100    SETTINGS_CACHE.lock().unwrap().plugin_settings_base = value;
101}
102
103/// Clear the plugin settings base layer.
104pub fn clear_plugin_settings_base() {
105    SETTINGS_CACHE.lock().unwrap().plugin_settings_base = None;
106}
107
108/// Clear all three caches. Called on settings write, plugin init, hooks refresh.
109pub fn reset_settings_cache() {
110    let mut cache = SETTINGS_CACHE.lock().unwrap();
111    cache.session_settings = None;
112    cache.per_source_cache.clear();
113    cache.parse_file_cache.clear();
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_session_cache_roundtrip() {
122        reset_settings_cache();
123        assert!(get_session_settings_cache().is_none());
124
125        let settings = SettingsWithErrors {
126            settings: serde_json::json!({"permissions": {"defaultMode": "allow"}}),
127            errors: vec![],
128        };
129        set_session_settings_cache(settings.clone());
130        assert_eq!(
131            get_session_settings_cache().as_ref(),
132            Some(&settings),
133        );
134    }
135
136    #[test]
137    fn test_per_source_cache() {
138        reset_settings_cache();
139        let source = SettingSource::UserSettings;
140        // Cache miss
141        assert!(get_cached_settings_for_source(&source).is_none());
142
143        // Set value
144        set_cached_settings_for_source(
145            &source,
146            Some(serde_json::json!({"permissions": {"defaultMode": "deny"}})),
147        );
148        assert!(get_cached_settings_for_source(&source).is_some());
149
150        // Set null (no settings for source)
151        set_cached_settings_for_source(&source, None);
152        assert!(get_cached_settings_for_source(&source).is_some());
153        assert!(get_cached_settings_for_source(&source).unwrap().is_none());
154    }
155
156    #[test]
157    fn test_parse_file_cache() {
158        reset_settings_cache();
159        assert!(get_cached_parsed_file("/nonexistent").is_none());
160
161        let parsed = ParsedSettings {
162            settings: Some(serde_json::json!({})),
163            errors: vec![],
164        };
165        set_cached_parsed_file("/test/path.json", parsed.clone());
166        assert_eq!(
167            get_cached_parsed_file("/test/path/path.json").as_ref(),
168            None,
169        );
170        assert_eq!(
171            get_cached_parsed_file("/test/path.json").as_ref(),
172            Some(&parsed),
173        );
174    }
175
176    #[test]
177    fn test_plugin_settings_base() {
178        reset_settings_cache();
179        assert!(get_plugin_settings_base().is_none());
180
181        set_plugin_settings_base(Some(serde_json::json!({"model": {"name": "test"}})));
182        assert!(get_plugin_settings_base().is_some());
183
184        clear_plugin_settings_base();
185        assert!(get_plugin_settings_base().is_none());
186    }
187
188    #[test]
189    fn test_reset_clears_all() {
190        // Populate all caches
191        set_session_settings_cache(SettingsWithErrors {
192            settings: serde_json::json!({}),
193            errors: vec![],
194        });
195        set_cached_settings_for_source(
196            &SettingSource::UserSettings,
197            Some(serde_json::json!({})),
198        );
199        set_cached_parsed_file(
200            "/test.json",
201            ParsedSettings {
202                settings: Some(serde_json::json!({})),
203                errors: vec![],
204            },
205        );
206        set_plugin_settings_base(Some(serde_json::json!({})));
207
208        reset_settings_cache();
209
210        assert!(get_session_settings_cache().is_none());
211        assert!(get_cached_settings_for_source(&SettingSource::UserSettings).is_none());
212        assert!(get_cached_parsed_file("/test.json").is_none());
213        // Plugin base is not cleared by reset (separate concern)
214        // Actually in TS it IS NOT cleared by resetSettingsCache
215    }
216}