Skip to main content

sift_core/index/
meta.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::search::filter::{CandidateFilter, VisibilityConfig};
6
7use super::config::{CorpusKind, CorpusSpec, IndexConfig, WalkOptions};
8use super::{IndexError, IndexKind};
9
10const META_FILE: &str = "meta.json";
11const STORE_VERSION: u32 = 1;
12
13/// Persistent store manifest (`.sift/meta.json`).
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct StoreMeta {
16    pub version: u32,
17    pub corpus: CorpusMeta,
18    pub walk: WalkMeta,
19    pub filters: FilterMeta,
20    pub indexes: Vec<IndexKind>,
21}
22
23/// Which corpus this store indexes.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CorpusMeta {
26    pub root: PathBuf,
27    pub kind: CorpusKind,
28    #[serde(default)]
29    pub include_paths: Vec<PathBuf>,
30    #[serde(default)]
31    pub exclude_paths: Vec<PathBuf>,
32}
33
34/// Filesystem walk behavior used when the index was built.
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
36pub struct WalkMeta {
37    pub follow_links: bool,
38    #[serde(default)]
39    pub one_file_system: bool,
40    #[serde(default)]
41    pub max_depth: Option<usize>,
42    #[serde(default)]
43    pub max_filesize: Option<u64>,
44}
45
46/// Ignore and visibility rules in effect when the index was built.
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct FilterMeta {
49    pub visibility: VisibilityConfig,
50}
51
52impl StoreMeta {
53    /// Create a new `StoreMeta` with the current store version.
54    #[must_use]
55    pub const fn new(
56        corpus: CorpusMeta,
57        walk: WalkMeta,
58        filters: FilterMeta,
59        indexes: Vec<IndexKind>,
60    ) -> Self {
61        Self {
62            version: STORE_VERSION,
63            corpus,
64            walk,
65            filters,
66            indexes,
67        }
68    }
69
70    /// Path to `meta.json` within `dir`.
71    #[must_use]
72    pub fn path(dir: &Path) -> PathBuf {
73        dir.join(META_FILE)
74    }
75
76    /// Read from `dir/meta.json`.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the file is missing or malformed.
81    pub fn read(dir: &Path) -> crate::Result<Self> {
82        let meta_path = Self::path(dir);
83        let raw = std::fs::read_to_string(&meta_path)?;
84        serde_json::from_str(&raw).map_err(|e| {
85            crate::Error::Index(IndexError::InvalidManifest {
86                path: meta_path,
87                source: e,
88            })
89        })
90    }
91
92    /// Write to `dir/meta.json`.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if serialization or writing fails.
97    pub fn write(&self, dir: &Path) -> crate::Result<()> {
98        let meta_path = Self::path(dir);
99        let json = serde_json::to_vec_pretty(self).map_err(|e| {
100            crate::Error::Index(IndexError::InvalidManifest {
101                path: meta_path.clone(),
102                source: e,
103            })
104        })?;
105        std::fs::write(&meta_path, json)?;
106        Ok(())
107    }
108
109    /// Map persisted metadata to a runtime index build configuration.
110    #[must_use]
111    pub fn index_config(&self) -> IndexConfig<'_> {
112        IndexConfig {
113            corpus: CorpusSpec {
114                root: &self.corpus.root,
115                kind: self.corpus.kind,
116                follow_links: self.walk.follow_links,
117                include_paths: &self.corpus.include_paths,
118                exclude_paths: &self.corpus.exclude_paths,
119            },
120            walk: WalkOptions {
121                follow_links: self.walk.follow_links,
122                one_file_system: self.walk.one_file_system,
123                max_depth: self.walk.max_depth,
124                max_filesize: self.walk.max_filesize,
125            },
126            visibility: self.filters.visibility.clone(),
127        }
128    }
129
130    /// Whether the search-time filter matches the walk and visibility stored in meta.
131    #[must_use]
132    pub fn matches_search_filter(&self, filter: &CandidateFilter) -> bool {
133        self.walk.follow_links == filter.follow_links()
134            && self.walk.one_file_system == filter.one_file_system()
135            && self.walk.max_depth == filter.max_depth()
136            && self.walk.max_filesize == filter.max_filesize()
137            && self.filters.visibility == *filter.visibility()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::search::filter::IgnoreConfig;
145    use tempfile::TempDir;
146
147    #[test]
148    fn read_write_roundtrip() {
149        let tmp = TempDir::new().expect("create temp dir");
150
151        let meta = StoreMeta::new(
152            CorpusMeta {
153                root: PathBuf::from("/some/root"),
154                kind: CorpusKind::Directory,
155                include_paths: vec![PathBuf::from("only.rs")],
156                exclude_paths: vec![PathBuf::from(".sift")],
157            },
158            WalkMeta {
159                follow_links: true,
160                one_file_system: true,
161                max_depth: Some(3),
162                max_filesize: Some(1024),
163            },
164            FilterMeta {
165                visibility: VisibilityConfig {
166                    ignore: IgnoreConfig::standard(),
167                    ..VisibilityConfig::default()
168                },
169            },
170            vec![IndexKind::Trigram],
171        );
172        meta.write(tmp.path()).expect("write meta");
173
174        let loaded = StoreMeta::read(tmp.path()).expect("read meta");
175        assert_eq!(loaded.version, meta.version);
176        assert_eq!(loaded.corpus.root, meta.corpus.root);
177        assert_eq!(loaded.corpus.kind, meta.corpus.kind);
178        assert_eq!(loaded.corpus.include_paths, meta.corpus.include_paths);
179        assert_eq!(loaded.walk, meta.walk);
180        assert_eq!(loaded.filters, meta.filters);
181        assert_eq!(loaded.indexes, meta.indexes);
182    }
183
184    #[test]
185    fn path_returns_meta_json() {
186        let p = StoreMeta::path(Path::new("/foo/bar"));
187        assert_eq!(p, PathBuf::from("/foo/bar/meta.json"));
188    }
189}