provenant/cache/
config.rs1use 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}