Skip to main content

provenant/cache/
config.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use clap::ValueEnum;
6
7pub const DEFAULT_CACHE_DIR_NAME: &str = ".provenant-cache";
8pub const CACHE_DIR_ENV_VAR: &str = "PROVENANT_CACHE";
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
11pub enum CacheKind {
12    #[value(alias = "scan")]
13    ScanResults,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub struct CacheKinds {
18    scan_results: bool,
19}
20
21impl CacheKinds {
22    pub fn from_cli(kinds: &[CacheKind]) -> Self {
23        let mut selected = Self::default();
24
25        for kind in kinds {
26            match kind {
27                CacheKind::ScanResults => selected.scan_results = true,
28            }
29        }
30
31        selected
32    }
33
34    pub const fn scan_results(self) -> bool {
35        self.scan_results
36    }
37
38    pub const fn any_enabled(self) -> bool {
39        self.scan_results
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct CacheConfig {
45    root_dir: PathBuf,
46    kinds: CacheKinds,
47}
48
49impl CacheConfig {
50    #[cfg(test)]
51    pub fn new(root_dir: PathBuf) -> Self {
52        Self {
53            root_dir,
54            kinds: CacheKinds::default(),
55        }
56    }
57
58    pub fn with_kinds(root_dir: PathBuf, kinds: CacheKinds) -> Self {
59        Self { root_dir, kinds }
60    }
61
62    #[cfg(test)]
63    pub fn from_scan_root(scan_root: &Path) -> Self {
64        Self::new(scan_root.join(DEFAULT_CACHE_DIR_NAME))
65    }
66
67    pub fn from_scan_root_with_kinds(scan_root: &Path, kinds: CacheKinds) -> Self {
68        Self::with_kinds(scan_root.join(DEFAULT_CACHE_DIR_NAME), kinds)
69    }
70
71    pub fn resolve_root_dir(
72        scan_root: &Path,
73        cli_cache_dir: Option<&Path>,
74        env_cache_dir: Option<&Path>,
75    ) -> PathBuf {
76        if let Some(path) = cli_cache_dir {
77            return path.to_path_buf();
78        }
79
80        if let Some(path) = env_cache_dir {
81            return path.to_path_buf();
82        }
83
84        scan_root.join(DEFAULT_CACHE_DIR_NAME)
85    }
86
87    pub fn from_overrides(
88        scan_root: &Path,
89        cli_cache_dir: Option<&Path>,
90        env_cache_dir: Option<&Path>,
91        kinds: CacheKinds,
92    ) -> Self {
93        if cli_cache_dir.is_none() && env_cache_dir.is_none() {
94            return Self::from_scan_root_with_kinds(scan_root, kinds);
95        }
96
97        Self::with_kinds(
98            Self::resolve_root_dir(scan_root, cli_cache_dir, env_cache_dir),
99            kinds,
100        )
101    }
102
103    pub fn root_dir(&self) -> &Path {
104        &self.root_dir
105    }
106
107    pub fn scan_results_dir(&self) -> PathBuf {
108        self.root_dir.join("scan-results")
109    }
110
111    pub const fn scan_results_enabled(&self) -> bool {
112        self.kinds.scan_results()
113    }
114
115    pub const fn any_enabled(&self) -> bool {
116        self.kinds.any_enabled()
117    }
118
119    pub fn ensure_dirs(&self) -> io::Result<()> {
120        if self.scan_results_enabled() {
121            fs::create_dir_all(self.scan_results_dir())?;
122        }
123        Ok(())
124    }
125
126    pub fn clear(&self) -> io::Result<()> {
127        if self.root_dir().exists() {
128            fs::remove_dir_all(&self.root_dir)?;
129        }
130        Ok(())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use tempfile::TempDir;
137
138    use super::*;
139
140    #[test]
141    fn test_from_scan_root_uses_expected_directory_name() {
142        let temp_dir = TempDir::new().expect("Failed to create temp dir");
143        let config = CacheConfig::from_scan_root(temp_dir.path());
144        assert_eq!(
145            config.root_dir(),
146            temp_dir.path().join(DEFAULT_CACHE_DIR_NAME)
147        );
148    }
149
150    #[test]
151    fn test_ensure_dirs_creates_expected_tree() {
152        let temp_dir = TempDir::new().expect("Failed to create temp dir");
153        let config = CacheConfig::from_scan_root_with_kinds(
154            temp_dir.path(),
155            CacheKinds { scan_results: true },
156        );
157
158        config
159            .ensure_dirs()
160            .expect("Failed to create cache directories");
161
162        assert!(config.root_dir().exists());
163        assert!(config.scan_results_dir().exists());
164    }
165
166    #[test]
167    fn test_ensure_dirs_only_creates_scan_results_subdirectory() {
168        let temp_dir = TempDir::new().expect("Failed to create temp dir");
169        let config = CacheConfig::from_scan_root_with_kinds(
170            temp_dir.path(),
171            CacheKinds { scan_results: true },
172        );
173
174        config
175            .ensure_dirs()
176            .expect("Failed to create selected cache directories");
177
178        assert!(config.scan_results_dir().exists());
179    }
180
181    #[test]
182    fn test_resolve_root_dir_prefers_cli_then_env_then_default() {
183        let scan_root = Path::new("/scan-root");
184        let cli_dir = Path::new("/cli-cache");
185        let env_dir = Path::new("/env-cache");
186
187        assert_eq!(
188            CacheConfig::resolve_root_dir(scan_root, Some(cli_dir), Some(env_dir)),
189            cli_dir
190        );
191        assert_eq!(
192            CacheConfig::resolve_root_dir(scan_root, None, Some(env_dir)),
193            env_dir
194        );
195        assert_eq!(
196            CacheConfig::resolve_root_dir(scan_root, None, None),
197            PathBuf::from(format!("/scan-root/{DEFAULT_CACHE_DIR_NAME}"))
198        );
199    }
200
201    #[test]
202    fn test_cache_kinds_from_cli_supports_scan_results() {
203        let selected = CacheKinds::from_cli(&[CacheKind::ScanResults]);
204        assert!(selected.scan_results());
205    }
206
207    #[test]
208    fn test_clear_removes_cache_root_directory() {
209        let temp_dir = TempDir::new().expect("Failed to create temp dir");
210        let config = CacheConfig::with_kinds(
211            temp_dir.path().join("cache-root"),
212            CacheKinds { scan_results: true },
213        );
214
215        config
216            .ensure_dirs()
217            .expect("Failed to create cache directories");
218        assert!(config.root_dir().exists());
219
220        config.clear().expect("Failed to clear cache directory");
221        assert!(!config.root_dir().exists());
222    }
223}