ripvec_core/cache/
config.rs1use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{Error, Result};
14
15#[derive(Debug, Serialize, Deserialize)]
22pub struct RepoConfig {
23 #[serde(default, skip_serializing_if = "IgnoreConfig::is_empty")]
25 pub ignore: IgnoreConfig,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
33pub struct IgnoreConfig {
34 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub patterns: Vec<String>,
37}
38
39impl IgnoreConfig {
40 #[must_use]
41 pub fn is_empty(&self) -> bool {
42 self.patterns.is_empty()
43 }
44}
45
46impl RepoConfig {
47 pub fn from_toml(s: &str) -> Result<Self> {
53 toml::from_str(s)
54 .map_err(|e| Error::Other(anyhow::anyhow!("failed to deserialize config: {e}")))
55 }
56
57 pub fn load(path: &Path) -> Result<Self> {
66 let file = path.join("config.toml");
67 let contents = std::fs::read_to_string(&file).map_err(|source| Error::Io {
68 path: file.display().to_string(),
69 source,
70 })?;
71 Self::from_toml(&contents)
72 }
73}
74
75#[must_use]
80pub fn find_config(start: &Path) -> Option<(PathBuf, RepoConfig)> {
81 let mut current = start.to_path_buf();
82 loop {
83 let candidate = current.join(".ripvec");
84 let config_file = candidate.join("config.toml");
85 if config_file.exists() {
86 return RepoConfig::load(&candidate)
87 .ok()
88 .map(|config| (candidate, config));
89 }
90 match current.parent() {
91 Some(parent) => current = parent.to_path_buf(),
92 None => return None,
93 }
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use tempfile::TempDir;
101
102 #[test]
103 fn round_trip_toml_ignore_only() {
104 let toml_str = "[ignore]\npatterns = [\"*.jsonl\", \"docs/generated/**\"]\n";
105 let cfg = RepoConfig::from_toml(toml_str).expect("deserialize");
106 assert_eq!(cfg.ignore.patterns, ["*.jsonl", "docs/generated/**"]);
107 }
108
109 #[test]
110 fn missing_ignore_section_defaults_to_empty_patterns() {
111 let cfg_str =
113 "[cache]\nlocal = true\nmodel = \"BAAI/bge-small-en-v1.5\"\nversion = \"3\"\n";
114 let cfg = RepoConfig::from_toml(cfg_str).expect("deserialize");
115 assert!(cfg.ignore.patterns.is_empty());
116 }
117
118 #[test]
119 fn load_from_disk() {
120 let dir = TempDir::new().expect("tempdir");
121 let ripvec_dir = dir.path().join(".ripvec");
122 std::fs::create_dir_all(&ripvec_dir).expect("mkdir");
123 let cfg_str = "[ignore]\npatterns = [\"*.log\"]\n";
124 std::fs::write(ripvec_dir.join("config.toml"), cfg_str).expect("write");
125 let loaded = RepoConfig::load(&ripvec_dir).expect("load");
126 assert_eq!(loaded.ignore.patterns, ["*.log"]);
127 }
128
129 #[test]
130 fn find_config_in_current_dir() {
131 let dir = TempDir::new().expect("tempdir");
132 let ripvec_dir = dir.path().join(".ripvec");
133 std::fs::create_dir_all(&ripvec_dir).expect("mkdir");
134 std::fs::write(
135 ripvec_dir.join("config.toml"),
136 "[ignore]\npatterns = [\"*.tmp\"]\n",
137 )
138 .expect("write");
139 let found = find_config(dir.path());
140 assert!(found.is_some());
141 let (_, cfg) = found.unwrap();
142 assert_eq!(cfg.ignore.patterns, ["*.tmp"]);
143 }
144
145 #[test]
146 fn find_config_in_parent_dir() {
147 let dir = TempDir::new().expect("tempdir");
148 let ripvec_dir = dir.path().join(".ripvec");
149 std::fs::create_dir_all(&ripvec_dir).expect("mkdir");
150 std::fs::write(
151 ripvec_dir.join("config.toml"),
152 "[ignore]\npatterns = [\"*.tmp\"]\n",
153 )
154 .expect("write");
155 let subdir = dir.path().join("src").join("foo");
156 std::fs::create_dir_all(&subdir).expect("mkdir");
157 let found = find_config(&subdir);
158 assert!(found.is_some(), "should walk up to parent .ripvec");
159 }
160
161 #[test]
162 fn find_config_not_found() {
163 let dir = TempDir::new().expect("tempdir");
164 assert!(find_config(dir.path()).is_none());
165 }
166}