Skip to main content

sqry_cli/persistence/
config.rs

1//! Configuration for query persistence.
2//!
3//! Handles path resolution, environment variable overrides, and default settings
4//! for the persistence subsystem.
5
6use std::path::PathBuf;
7
8/// Default maximum number of history entries to retain.
9pub const DEFAULT_MAX_HISTORY_ENTRIES: usize = 10_000;
10
11/// Default maximum size of the user metadata index in bytes (10 MB).
12pub const DEFAULT_MAX_INDEX_BYTES: u64 = 10 * 1024 * 1024;
13
14/// Environment variable for overriding the global config directory.
15pub const ENV_CONFIG_DIR: &str = "SQRY_CONFIG_DIR";
16
17/// Environment variable to disable history recording.
18pub const ENV_NO_HISTORY: &str = "SQRY_NO_HISTORY";
19
20/// Environment variable to disable secret redaction (default: enabled).
21pub const ENV_NO_REDACT: &str = "SQRY_NO_REDACT";
22
23/// Configuration for the persistence subsystem.
24#[derive(Debug, Clone)]
25pub struct PersistenceConfig {
26    /// Override for global config directory (from CLI or env)
27    pub global_dir_override: Option<PathBuf>,
28
29    /// Override for local config directory (for testing)
30    pub local_dir_override: Option<PathBuf>,
31
32    /// Whether history recording is enabled
33    pub history_enabled: bool,
34
35    /// Maximum history entries to retain
36    pub max_history_entries: usize,
37
38    /// Maximum index size in bytes before rotation
39    pub max_index_bytes: u64,
40
41    /// Whether to redact detected secrets from history
42    pub redact_secrets: bool,
43}
44
45impl Default for PersistenceConfig {
46    fn default() -> Self {
47        Self {
48            global_dir_override: None,
49            local_dir_override: None,
50            history_enabled: true,
51            max_history_entries: DEFAULT_MAX_HISTORY_ENTRIES,
52            max_index_bytes: DEFAULT_MAX_INDEX_BYTES,
53            // Default to true for privacy - users can opt out via SQRY_NO_REDACT=1
54            redact_secrets: true,
55        }
56    }
57}
58
59impl PersistenceConfig {
60    /// Create config from environment variables only.
61    ///
62    /// This is a convenience method for when CLI options are not available.
63    #[must_use]
64    pub fn from_env() -> Self {
65        Self::from_env_and_cli(None, None, false)
66    }
67
68    /// Create config from environment variables and CLI overrides.
69    ///
70    /// CLI options take precedence over environment variables, which take
71    /// precedence over defaults.
72    ///
73    /// # Arguments
74    ///
75    /// * `cli_config_dir` - Config directory override from CLI
76    /// * `cli_max_history` - Max history entries from CLI config file
77    /// * `cli_no_history` - Whether `--no-history` was passed
78    #[must_use]
79    pub fn from_env_and_cli(
80        cli_config_dir: Option<PathBuf>,
81        cli_max_history: Option<usize>,
82        cli_no_history: bool,
83    ) -> Self {
84        // Config dir: CLI > ENV > default
85        let global_dir_override =
86            cli_config_dir.or_else(|| std::env::var(ENV_CONFIG_DIR).ok().map(PathBuf::from));
87
88        // History enabled: CLI --no-history > ENV > default (true)
89        let history_enabled = if cli_no_history {
90            false
91        } else {
92            std::env::var(ENV_NO_HISTORY)
93                .map(|v| !["1", "true", "yes"].contains(&v.to_lowercase().as_str()))
94                .unwrap_or(true)
95        };
96
97        // Redact secrets: default true, disable via SQRY_NO_REDACT=1
98        let redact_secrets = std::env::var(ENV_NO_REDACT)
99            .map(|v| !["1", "true", "yes"].contains(&v.to_lowercase().as_str()))
100            .unwrap_or(true);
101
102        Self {
103            global_dir_override,
104            local_dir_override: None,
105            history_enabled,
106            max_history_entries: cli_max_history.unwrap_or(DEFAULT_MAX_HISTORY_ENTRIES),
107            max_index_bytes: DEFAULT_MAX_INDEX_BYTES,
108            redact_secrets,
109        }
110    }
111
112    /// Get the global config directory path.
113    ///
114    /// Uses the override if set, otherwise returns the platform-specific
115    /// config directory.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the config directory cannot be determined.
120    pub fn global_config_dir(&self) -> anyhow::Result<PathBuf> {
121        if let Some(ref override_path) = self.global_dir_override {
122            return Ok(override_path.clone());
123        }
124
125        // Use dirs crate for cross-platform paths
126        dirs::config_dir()
127            .map(|p| p.join("sqry"))
128            .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))
129    }
130
131    /// Get the local config directory path.
132    ///
133    /// For local storage, this is the project root where `.sqry-index.user`
134    /// will be stored.
135    ///
136    /// # Arguments
137    ///
138    /// * `project_root` - The project root directory
139    #[must_use]
140    pub fn local_config_dir(&self, project_root: &std::path::Path) -> PathBuf {
141        self.local_dir_override
142            .clone()
143            .unwrap_or_else(|| project_root.to_path_buf())
144    }
145}
146
147/// Get the default global config directory.
148///
149/// This is a convenience function for when you don't have a full config.
150///
151/// # Errors
152///
153/// Returns an error if the config directory cannot be determined.
154pub fn global_config_dir() -> anyhow::Result<PathBuf> {
155    dirs::config_dir()
156        .map(|p| p.join("sqry"))
157        .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use serial_test::serial;
164
165    #[test]
166    fn test_default_config() {
167        let config = PersistenceConfig::default();
168
169        assert!(config.global_dir_override.is_none());
170        assert!(config.local_dir_override.is_none());
171        assert!(config.history_enabled);
172        assert_eq!(config.max_history_entries, DEFAULT_MAX_HISTORY_ENTRIES);
173        assert_eq!(config.max_index_bytes, DEFAULT_MAX_INDEX_BYTES);
174        // Default is now true for privacy
175        assert!(
176            config.redact_secrets,
177            "redact_secrets should default to true"
178        );
179    }
180
181    #[test]
182    #[serial]
183    fn test_cli_config_dir_takes_precedence() {
184        // Set env var
185        unsafe {
186            std::env::set_var(ENV_CONFIG_DIR, "/env/path");
187        }
188
189        let config =
190            PersistenceConfig::from_env_and_cli(Some(PathBuf::from("/cli/path")), None, false);
191
192        assert_eq!(config.global_dir_override, Some(PathBuf::from("/cli/path")));
193
194        unsafe {
195            std::env::remove_var(ENV_CONFIG_DIR);
196        }
197    }
198
199    #[test]
200    #[serial]
201    fn test_env_config_dir_fallback() {
202        unsafe {
203            std::env::set_var(ENV_CONFIG_DIR, "/env/path");
204        }
205
206        let config = PersistenceConfig::from_env_and_cli(None, None, false);
207
208        assert_eq!(config.global_dir_override, Some(PathBuf::from("/env/path")));
209
210        unsafe {
211            std::env::remove_var(ENV_CONFIG_DIR);
212        }
213    }
214
215    #[test]
216    fn test_cli_no_history_disables_history() {
217        let config = PersistenceConfig::from_env_and_cli(None, None, true);
218
219        assert!(!config.history_enabled);
220    }
221
222    #[test]
223    #[serial]
224    fn test_env_no_history_disables_history() {
225        unsafe {
226            std::env::set_var(ENV_NO_HISTORY, "1");
227        }
228
229        let config = PersistenceConfig::from_env_and_cli(None, None, false);
230
231        assert!(!config.history_enabled);
232
233        unsafe {
234            std::env::remove_var(ENV_NO_HISTORY);
235        }
236    }
237
238    #[test]
239    #[serial]
240    fn test_redact_secrets_default_enabled() {
241        // Default is now true (enabled)
242        let config = PersistenceConfig::from_env_and_cli(None, None, false);
243        assert!(
244            config.redact_secrets,
245            "redact_secrets should default to true"
246        );
247    }
248
249    #[test]
250    #[serial]
251    fn test_redact_secrets_disabled_via_env() {
252        unsafe {
253            std::env::set_var(ENV_NO_REDACT, "1");
254        }
255
256        let config = PersistenceConfig::from_env_and_cli(None, None, false);
257
258        assert!(
259            !config.redact_secrets,
260            "SQRY_NO_REDACT=1 should disable redaction"
261        );
262
263        unsafe {
264            std::env::remove_var(ENV_NO_REDACT);
265        }
266    }
267
268    #[test]
269    fn test_global_config_dir_with_override() {
270        let config = PersistenceConfig {
271            global_dir_override: Some(PathBuf::from("/custom/path")),
272            ..Default::default()
273        };
274
275        let dir = config.global_config_dir().expect("should succeed");
276        assert_eq!(dir, PathBuf::from("/custom/path"));
277    }
278
279    #[test]
280    fn test_local_config_dir() {
281        let config = PersistenceConfig::default();
282        let project = PathBuf::from("/home/user/project");
283
284        let local_dir = config.local_config_dir(&project);
285        assert_eq!(local_dir, project);
286    }
287
288    #[test]
289    fn test_local_config_dir_with_override() {
290        let config = PersistenceConfig {
291            local_dir_override: Some(PathBuf::from("/custom/local")),
292            ..Default::default()
293        };
294        let project = PathBuf::from("/home/user/project");
295
296        let local_dir = config.local_config_dir(&project);
297        assert_eq!(local_dir, PathBuf::from("/custom/local"));
298    }
299}