Skip to main content

zenith_session/
datadir.rs

1//! Platform data-directory resolution for zenith-session.
2//!
3//! The resolved directory is the root under which all `.zen` store data lives.
4//!
5//! # Platform mapping (via the `dirs` crate)
6//!
7//! | Platform | Path |
8//! |----------|------|
9//! | Linux    | `$XDG_DATA_HOME/zenith` (falls back to `~/.local/share/zenith`) |
10//! | macOS    | `~/Library/Application Support/zenith` |
11//! | Windows  | `%LOCALAPPDATA%\zenith` |
12//!
13//! # Override
14//!
15//! Set `ZENITH_DATA_DIR` to a non-empty path to bypass platform detection
16//! entirely.  Useful for testing and for portable installations.
17
18use std::path::PathBuf;
19
20use crate::error::SessionError;
21
22/// Resolve the zenith data directory using an injectable environment lookup.
23///
24/// Priority:
25/// 1. `ZENITH_DATA_DIR` environment variable, if non-empty.
26/// 2. `dirs::data_dir()` joined with `"zenith"`.
27///
28/// Returns [`SessionError`] if neither source is available (e.g. on a headless
29/// system where the platform data dir cannot be determined).
30///
31/// The injectable `env` parameter makes this function fully deterministic in
32/// tests — pass a closure that returns `Some(...)` for the keys you want to
33/// override, `None` otherwise.
34pub fn resolve_data_dir_with(
35    env: impl Fn(&str) -> Option<String>,
36) -> Result<PathBuf, SessionError> {
37    if let Some(val) = env("ZENITH_DATA_DIR")
38        && !val.is_empty()
39    {
40        return Ok(PathBuf::from(val));
41    }
42    dirs::data_dir().map(|d| d.join("zenith")).ok_or_else(|| {
43        SessionError::new(
44            "cannot determine data directory \
45                 (no ZENITH_DATA_DIR and platform data dir unavailable)",
46        )
47    })
48}
49
50/// Resolve the zenith data directory using real environment variables.
51///
52/// Wraps [`resolve_data_dir_with`] with [`std::env::var`].
53pub fn resolve_data_dir() -> Result<PathBuf, SessionError> {
54    resolve_data_dir_with(|k| std::env::var(k).ok())
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn custom_override_is_used() {
63        let result = resolve_data_dir_with(|_| Some("/custom/path".into()));
64        assert_eq!(result.unwrap(), PathBuf::from("/custom/path"));
65    }
66
67    #[test]
68    fn empty_override_falls_through_to_platform() {
69        // Empty string must be ignored; result is either Ok (path ending in
70        // "zenith") or Err (no platform data dir in CI).
71        let result = resolve_data_dir_with(|k| {
72            if k == "ZENITH_DATA_DIR" {
73                Some(String::new()) // empty — must fall through
74            } else {
75                None
76            }
77        });
78        match result {
79            Ok(path) => {
80                assert!(
81                    path.ends_with("zenith"),
82                    "expected path to end with 'zenith', got: {}",
83                    path.display()
84                );
85            }
86            Err(_) => {
87                // Acceptable in CI environments with no platform data dir.
88            }
89        }
90    }
91
92    #[test]
93    fn no_override_yields_platform_or_err() {
94        let result = resolve_data_dir_with(|_| None);
95        match result {
96            Ok(path) => {
97                assert!(
98                    path.ends_with("zenith"),
99                    "expected path to end with 'zenith', got: {}",
100                    path.display()
101                );
102            }
103            Err(e) => {
104                assert!(e.message.contains("cannot determine data directory"));
105            }
106        }
107    }
108}