Skip to main content

ripvec_core/cache/
config.rs

1//! Repository-local configuration for ripvec.
2//!
3//! Discovers `.ripvec/config.toml` files that carry repo-local search settings
4//! (extension whitelists, ignore globs). Post-v3.0.0 the cache/model fields
5//! (`CacheConfig`) and the `find_repo_config` / `save` / `to_toml` helpers
6//! were removed: the engine is cacheless and those items had zero non-test
7//! callers after surgery.
8
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{Error, Result};
14
15/// Top-level structure for `.ripvec/config.toml`.
16///
17/// Only the `ignore` section is consumed by the live engine. The former
18/// `[cache]` section (model, version, local flag) is accepted by serde but
19/// silently skipped; existing config files that still carry `[cache]` will
20/// continue to parse without error.
21#[derive(Debug, Serialize, Deserialize)]
22pub struct RepoConfig {
23    /// Index ignore settings for this repository.
24    #[serde(default, skip_serializing_if = "IgnoreConfig::is_empty")]
25    pub ignore: IgnoreConfig,
26}
27
28/// Ignore settings stored in `.ripvec/config.toml`.
29///
30/// Patterns use `.gitignore` syntax and are matched relative to the repository
31/// root. Examples: `"*.jsonl"`, `"docs/generated/**"`, `"!docs/keep.md"`.
32#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
33pub struct IgnoreConfig {
34    /// Additional gitignore-style patterns to exclude from the ripvec index.
35    #[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    /// Deserialize from a TOML string.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`Error::Other`] if deserialization fails.
52    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    /// Load config from `<path>/config.toml`.
58    ///
59    /// `path` should be the `.ripvec/` directory.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`Error::Io`] if the file cannot be read, or [`Error::Other`]
64    /// on parse failure.
65    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/// Walk up the directory tree from `start` looking for `.ripvec/config.toml`.
76///
77/// Returns the `.ripvec/` directory and parsed config regardless of any
78/// cache settings. Used for non-cache settings such as index ignores.
79#[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        // Legacy config files only have [cache]; ignore section absent defaults to empty.
112        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}