Skip to main content

rlean_search/
cache.rs

1//! On-disk gzipped XML cache for one-shot invocations (`index.xml.gz`).
2
3use crate::index::SearchIndex;
4use crate::xml::{read_index_file, write_index_file};
5use anyhow::Result;
6use std::path::{Path, PathBuf};
7
8pub const DEFAULT_CACHE_DIR: &str = ".rlean-search";
9/// Default cache filename: gzipped XML at the fastest compression level.
10pub const DEFAULT_CACHE_FILE: &str = "index.xml.gz";
11
12pub fn default_cache_path(root: impl AsRef<Path>) -> PathBuf {
13    root.as_ref()
14        .join(DEFAULT_CACHE_DIR)
15        .join(DEFAULT_CACHE_FILE)
16}
17
18/// Load cache if present and `source_hash` matches expected (when provided).
19pub fn load_cache(path: impl AsRef<Path>, expected_hash: Option<&str>) -> Result<Option<SearchIndex>> {
20    let path = path.as_ref();
21    if !path.exists() {
22        return Ok(None);
23    }
24    let doc = read_index_file(path)?;
25    if let Some(exp) = expected_hash {
26        if doc.source_hash != exp {
27            tracing::info!("cache hash mismatch; rebuilding");
28            return Ok(None);
29        }
30    }
31    Ok(Some(SearchIndex::from_document(doc)))
32}
33
34pub fn save_cache(path: impl AsRef<Path>, index: &SearchIndex) -> Result<()> {
35    write_index_file(path.as_ref(), &index.doc)
36}
37
38/// Try load cache; if missing, build from paths and save.
39pub fn load_or_build(
40    paths: &[impl AsRef<Path>],
41    cache_path: impl AsRef<Path>,
42    force_rebuild: bool,
43) -> Result<SearchIndex> {
44    if !force_rebuild {
45        if let Some(idx) = load_cache(cache_path.as_ref(), None)? {
46            // Validate lightly: non-empty or empty paths
47            if !idx.is_empty() || paths.is_empty() {
48                return Ok(idx);
49            }
50        }
51    }
52    let idx = crate::index::build_index(paths)?;
53    save_cache(cache_path.as_ref(), &idx)?;
54    Ok(idx)
55}