Skip to main content

voxora_config/
cache.rs

1//! Cache directory configuration.
2//!
3//! Cascade (first non-empty wins):
4//! 1. `VoxoraConfig::cache.root` (explicit override)
5//! 2. `VOXORA_CACHE_DIR` env var
6//! 3. `$XDG_CACHE_HOME/voxora` (or `dirs::cache_dir()/voxora`)
7//! 4. `.voxora-cache` (relative; last-resort cross-platform fallback)
8
9use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13/// Where voxora keeps downloaded models on disk.
14#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(default, deny_unknown_fields)]
16#[non_exhaustive]
17pub struct CacheConfig {
18    /// Explicit override. `None` defers to env / XDG / fallback.
19    pub root: Option<PathBuf>,
20}
21
22impl CacheConfig {
23    /// Build a [`CacheConfig`] from its field. Provided because the
24    /// type is `#[non_exhaustive]` and cannot be built with a struct
25    /// expression from outside this crate.
26    pub fn new(root: Option<PathBuf>) -> Self {
27        Self { root }
28    }
29
30    /// Resolve the cache directory honouring the cascade documented at
31    /// the top of this module.
32    pub fn resolve(&self) -> PathBuf {
33        if let Some(p) = &self.root {
34            return p.clone();
35        }
36        if let Ok(custom) = std::env::var(crate::env::VOXORA_CACHE_DIR)
37            && !custom.is_empty()
38        {
39            return PathBuf::from(custom);
40        }
41        if let Some(base) = dirs::cache_dir() {
42            return base.join("voxora");
43        }
44        // Last-resort fallback: relative path so the code still works
45        // even on platforms without $HOME (rare). On Linux/macOS/Windows
46        // dirs::cache_dir() should never be None.
47        PathBuf::from(".voxora-cache")
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn default_resolves_via_xdg_or_fallback() {
57        let cfg = CacheConfig::default();
58        let resolved = cfg.resolve();
59        assert!(!resolved.as_os_str().is_empty());
60        // Either XDG-based or the relative fallback — both end in
61        // "voxora" (XDG) or "voxora-cache" (last-resort).
62        let s = resolved.to_string_lossy();
63        assert!(
64            s.ends_with("voxora") || s.ends_with("voxora-cache"),
65            "got {s:?}"
66        );
67    }
68
69    #[test]
70    fn explicit_root_wins() {
71        let cfg = CacheConfig {
72            root: Some(PathBuf::from("/tmp/explicit")),
73        };
74        assert_eq!(cfg.resolve(), PathBuf::from("/tmp/explicit"));
75    }
76
77    #[test]
78    fn new_matches_struct_expression() {
79        let cfg = CacheConfig::new(Some(PathBuf::from("/tmp/via-new")));
80        assert_eq!(
81            cfg,
82            CacheConfig {
83                root: Some(PathBuf::from("/tmp/via-new")),
84            }
85        );
86    }
87}