ripvec_core/cache/
config.rs1use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{Error, Result};
11
12#[derive(Debug, Serialize, Deserialize)]
14pub struct RepoConfig {
15 pub cache: CacheConfig,
17 #[serde(default, skip_serializing_if = "IgnoreConfig::is_empty")]
19 pub ignore: IgnoreConfig,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
24pub struct CacheConfig {
25 pub local: bool,
27 pub model: String,
29 pub version: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub auto_stash: Option<bool>,
35}
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
42pub struct IgnoreConfig {
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub patterns: Vec<String>,
46}
47
48impl IgnoreConfig {
49 #[must_use]
50 pub fn is_empty(&self) -> bool {
51 self.patterns.is_empty()
52 }
53}
54
55impl RepoConfig {
56 #[must_use]
58 pub fn new(model: impl Into<String>, version: impl Into<String>) -> Self {
59 Self {
60 cache: CacheConfig {
61 local: true,
62 model: model.into(),
63 version: version.into(),
64 auto_stash: None,
65 },
66 ignore: IgnoreConfig::default(),
67 }
68 }
69
70 pub fn to_toml(&self) -> Result<String> {
76 toml::to_string(self)
77 .map_err(|e| Error::Other(anyhow::anyhow!("failed to serialize config: {e}")))
78 }
79
80 pub fn from_toml(s: &str) -> Result<Self> {
86 toml::from_str(s)
87 .map_err(|e| Error::Other(anyhow::anyhow!("failed to deserialize config: {e}")))
88 }
89
90 pub fn save(&self, path: &Path) -> Result<()> {
99 std::fs::create_dir_all(path).map_err(|source| Error::Io {
100 path: path.display().to_string(),
101 source,
102 })?;
103 let file = path.join("config.toml");
104 let contents = self.to_toml()?;
105 std::fs::write(&file, contents).map_err(|source| Error::Io {
106 path: file.display().to_string(),
107 source,
108 })
109 }
110
111 pub fn load(path: &Path) -> Result<Self> {
120 let file = path.join("config.toml");
121 let contents = std::fs::read_to_string(&file).map_err(|source| Error::Io {
122 path: file.display().to_string(),
123 source,
124 })?;
125 Self::from_toml(&contents)
126 }
127}
128
129#[must_use]
135pub fn find_repo_config(start: &Path) -> Option<PathBuf> {
136 find_config(start)
137 .filter(|(_, cfg)| cfg.cache.local)
138 .map(|(ripvec_dir, _)| ripvec_dir)
139}
140
141#[must_use]
146pub fn find_config(start: &Path) -> Option<(PathBuf, RepoConfig)> {
147 let mut current = start.to_path_buf();
148 loop {
149 let candidate = current.join(".ripvec");
150 let config_file = candidate.join("config.toml");
151 if config_file.exists() {
152 return RepoConfig::load(&candidate)
153 .ok()
154 .map(|config| (candidate, config));
155 }
156 match current.parent() {
157 Some(parent) => current = parent.to_path_buf(),
158 None => return None,
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use tempfile::TempDir;
167
168 #[test]
169 fn round_trip_toml() {
170 let cfg = RepoConfig::new("BAAI/bge-small-en-v1.5", "3");
171 let toml_str = cfg.to_toml().expect("serialize");
172 let restored = RepoConfig::from_toml(&toml_str).expect("deserialize");
173 assert!(restored.cache.local);
174 assert_eq!(restored.cache.model, "BAAI/bge-small-en-v1.5");
175 assert_eq!(restored.cache.version, "3");
176 }
177
178 #[test]
179 fn round_trip_ignore_patterns() {
180 let cfg = RepoConfig {
181 cache: CacheConfig {
182 local: true,
183 model: "nomic-ai/modernbert-embed-base".to_string(),
184 version: "3".to_string(),
185 auto_stash: None,
186 },
187 ignore: IgnoreConfig {
188 patterns: vec!["*.jsonl".to_string(), "docs/generated/**".to_string()],
189 },
190 };
191
192 let toml_str = cfg.to_toml().expect("serialize");
193
194 assert!(toml_str.contains("[ignore]"));
195 assert!(toml_str.contains("patterns"));
196 let restored = RepoConfig::from_toml(&toml_str).expect("deserialize");
197 assert_eq!(restored.ignore.patterns, ["*.jsonl", "docs/generated/**"]);
198 }
199
200 #[test]
201 fn missing_ignore_section_defaults_to_empty_patterns() {
202 let cfg_str =
203 "[cache]\nlocal = true\nmodel = \"BAAI/bge-small-en-v1.5\"\nversion = \"3\"\n";
204 let cfg = RepoConfig::from_toml(cfg_str).expect("deserialize");
205 assert!(cfg.ignore.patterns.is_empty());
206 }
207
208 #[test]
209 fn save_and_load() {
210 let dir = TempDir::new().expect("tempdir");
211 let ripvec_dir = dir.path().join(".ripvec");
212 let cfg = RepoConfig::new("nomic-ai/modernbert-embed-base", "3");
213 cfg.save(&ripvec_dir).expect("save");
214 assert!(ripvec_dir.join("config.toml").exists());
215 let loaded = RepoConfig::load(&ripvec_dir).expect("load");
216 assert!(loaded.cache.local);
217 assert_eq!(loaded.cache.model, "nomic-ai/modernbert-embed-base");
218 assert_eq!(loaded.cache.version, "3");
219 }
220
221 #[test]
222 fn find_repo_config_in_current_dir() {
223 let dir = TempDir::new().expect("tempdir");
224 let ripvec_dir = dir.path().join(".ripvec");
225 RepoConfig::new("BAAI/bge-small-en-v1.5", "3")
226 .save(&ripvec_dir)
227 .expect("save");
228 let found = find_repo_config(dir.path());
229 assert_eq!(found.as_deref(), Some(ripvec_dir.as_path()));
230 }
231
232 #[test]
233 fn find_repo_config_in_parent_dir() {
234 let dir = TempDir::new().expect("tempdir");
235 let ripvec_dir = dir.path().join(".ripvec");
236 RepoConfig::new("BAAI/bge-small-en-v1.5", "3")
237 .save(&ripvec_dir)
238 .expect("save");
239 let subdir = dir.path().join("src").join("foo");
240 std::fs::create_dir_all(&subdir).expect("mkdir");
241 let found = find_repo_config(&subdir);
242 assert_eq!(found.as_deref(), Some(ripvec_dir.as_path()));
243 }
244
245 #[test]
246 fn find_repo_config_not_found() {
247 let dir = TempDir::new().expect("tempdir");
248 assert!(find_repo_config(dir.path()).is_none());
250 }
251
252 #[test]
253 fn find_repo_config_ignores_disabled() {
254 let dir = TempDir::new().expect("tempdir");
255 let ripvec_dir = dir.path().join(".ripvec");
256 std::fs::create_dir_all(&ripvec_dir).expect("mkdir");
258 let cfg_str =
259 "[cache]\nlocal = false\nmodel = \"BAAI/bge-small-en-v1.5\"\nversion = \"3\"\n";
260 std::fs::write(ripvec_dir.join("config.toml"), cfg_str).expect("write");
261 assert!(find_repo_config(dir.path()).is_none());
262 }
263}