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}