cfgmatic-paths 1.1.2

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Unix/Linux directory finder (XDG Base Directory Specification).

use super::DirectoryFinder;
use crate::env::Env;
use std::path::PathBuf;

/// XDG-compliant directory finder for Unix/Linux systems.
///
/// Also used for macOS CLI tools, as they follow XDG conventions.
#[derive(Debug, Clone)]
pub struct UnixDirectoryFinder {
    /// Application name used in paths.
    app_name: String,
    /// Whether to include legacy `~/.apprc` fallback.
    legacy_rc: bool,
}

impl UnixDirectoryFinder {
    /// Create a new finder for Unix/Linux.
    pub fn new(app_name: impl Into<String>) -> Self {
        Self {
            app_name: app_name.into(),
            legacy_rc: true,
        }
    }

    /// Enable/disable legacy `~/.apprc` fallback.
    #[must_use]
    pub const fn legacy_rc(mut self, enabled: bool) -> Self {
        self.legacy_rc = enabled;
        self
    }

    /// Get `XDG_CONFIG_HOME` with fallback to ~/.config.
    fn config_home(env: &dyn Env) -> PathBuf {
        env.get("XDG_CONFIG_HOME")
            .map(PathBuf::from)
            .or_else(|| env.home_dir().map(|h| h.join(".config")))
            .unwrap_or_else(|| PathBuf::from("/").join(".config"))
    }

    /// Get `XDG_CONFIG_DIRS` with fallback to /etc/xdg.
    fn xdg_config_dirs(env: &dyn Env) -> Vec<PathBuf> {
        env.get("XDG_CONFIG_DIRS").map_or_else(
            || vec![PathBuf::from("/etc/xdg")],
            |dirs| dirs.split(':').map(PathBuf::from).collect(),
        )
    }
}

impl DirectoryFinder for UnixDirectoryFinder {
    fn user_dirs(&self, env: &dyn Env) -> Vec<PathBuf> {
        let mut paths = Vec::new();

        // $XDG_CONFIG_HOME/appname/
        paths.push(Self::config_home(env).join(&self.app_name));

        // Legacy ~/.apprc
        if self.legacy_rc
            && let Some(home) = env.home_dir()
        {
            let rc_name = format!(".{}rc", self.app_name);
            paths.push(home.join(rc_name));
        }

        paths
    }

    fn local_dirs(&self, _env: &dyn Env) -> Vec<PathBuf> {
        vec![
            PathBuf::from("/usr/local/etc").join(&self.app_name),
            PathBuf::from("/opt").join(&self.app_name).join("etc"),
        ]
    }

    fn system_dirs(&self, env: &dyn Env) -> Vec<PathBuf> {
        let mut paths = Vec::new();

        // XDG_CONFIG_DIRS/appname/
        for dir in Self::xdg_config_dirs(env) {
            paths.push(dir.join(&self.app_name));
        }

        // Fallback: /etc/appname
        paths.push(PathBuf::from("/etc").join(&self.app_name));

        paths
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct TestEnv {
        vars: std::collections::HashMap<String, String>,
        home: PathBuf,
    }

    impl TestEnv {
        fn new() -> Self {
            Self {
                vars: std::collections::HashMap::new(),
                home: PathBuf::from("/home/user"),
            }
        }

        fn with_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
            self.vars.insert(key.into(), value.into());
            self
        }
    }

    impl Env for TestEnv {
        fn get(&self, key: &str) -> Option<String> {
            self.vars.get(key).cloned()
        }

        fn home_dir(&self) -> Option<PathBuf> {
            Some(self.home.clone())
        }
    }

    #[test]
    fn test_user_dirs_with_xdg() {
        let env = TestEnv::new().with_var("XDG_CONFIG_HOME", "/custom/config");
        let finder = UnixDirectoryFinder::new("myapp");
        let dirs = finder.user_dirs(&env);

        assert_eq!(
            dirs,
            vec![
                PathBuf::from("/custom/config/myapp"),
                PathBuf::from("/home/user/.myapprc"),
            ]
        );
    }

    #[test]
    fn test_user_dirs_fallback() {
        let env = TestEnv::new();
        let finder = UnixDirectoryFinder::new("myapp");
        let dirs = finder.user_dirs(&env);

        assert_eq!(dirs[0], PathBuf::from("/home/user/.config/myapp"));
    }

    #[test]
    fn test_no_legacy() {
        let env = TestEnv::new();
        let finder = UnixDirectoryFinder::new("myapp").legacy_rc(false);
        let dirs = finder.user_dirs(&env);

        assert_eq!(dirs.len(), 1);
        assert_eq!(dirs[0], PathBuf::from("/home/user/.config/myapp"));
    }

    #[test]
    fn test_system_dirs() {
        let env = TestEnv::new().with_var("XDG_CONFIG_DIRS", "/etc/xdg:/opt/config");
        let finder = UnixDirectoryFinder::new("myapp");
        let dirs = finder.system_dirs(&env);

        assert_eq!(
            dirs,
            vec![
                PathBuf::from("/etc/xdg/myapp"),
                PathBuf::from("/opt/config/myapp"),
                PathBuf::from("/etc/myapp"),
            ]
        );
    }

    #[test]
    fn test_local_dirs() {
        let env = TestEnv::new();
        let finder = UnixDirectoryFinder::new("myapp");
        let dirs = finder.local_dirs(&env);

        assert_eq!(
            dirs,
            vec![
                PathBuf::from("/usr/local/etc/myapp"),
                PathBuf::from("/opt/myapp/etc"),
            ]
        );
    }
}