hierconf-core 0.2.0

Core functionality for hierconf configuration management
Documentation
use crate::attrs::{AttrKey, extract_attr};
use crate::error::HierConfError;
use camino::Utf8PathBuf;
use facet::Facet;
#[cfg(feature = "user-config")]
use std::env;
use std::fs;

/// Get all possible configuration file paths for an application, in order of precedence.
///
/// Returns paths with display strings for documentation (e.g., generating man pages).
/// See [`load_config`](crate::load_config) for path details.
pub fn config_paths<T>() -> Result<Vec<(Option<Utf8PathBuf>, String)>, HierConfError>
where
    T: Facet<'static>,
{
    let app_name = extract_attr::<T>(AttrKey::AppName)?;
    let mut paths = Vec::new();

    // Helper to add a config file path from a base directory with display string
    let mut add_config_path = |base: Option<Utf8PathBuf>, display: &str| {
        let path = base.map(|b| b.join(&app_name).join("config.toml"));
        let display_path = format!("{}/{}/config.toml", display, app_name);
        paths.push((path, display_path));
    };

    // Test-only: highest priority path for testing
    // Checks for test config files at runtime (only used when test files exist)
    #[cfg(feature = "test-paths")]
    {
        let manifest_dir = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let test_path_opt = manifest_dir
            .parent() // crates
            .map(|p| p.join("hierconf").join("tests"));

        add_config_path(test_path_opt, "tests");
    }

    // Add user provided config files first (highest precedence, only if user-config feature is enabled)
    // Always add these paths for documentation purposes, resolve path if env vars are set
    #[cfg(feature = "user-config")]
    {
        #[cfg(target_os = "macos")]
        {
            let config_home = env::var("HOME")
                .ok()
                .map(|home| Utf8PathBuf::from(home).join("Library/Application Support"));
            add_config_path(config_home, "${HOME}/Library/Application Support");
        }
        #[cfg(not(target_os = "macos"))]
        {
            let config_home = env::var("XDG_CONFIG_HOME")
                .ok()
                .map(Utf8PathBuf::from)
                .or_else(|| {
                    env::var("HOME")
                        .ok()
                        .map(|home| Utf8PathBuf::from(home).join(".config"))
                });
            add_config_path(config_home, "${XDG_CONFIG_HOME:-${HOME}/.config}");
        }
    }

    // System-wide configuration (second precedence)
    add_config_path(Some(Utf8PathBuf::from("/etc")), "/etc");

    // Distribution-provided defaults (lowest precedence)
    // Check for distribution prefix at compile time (e.g., for Homebrew: /opt/homebrew/etc)
    if let Some(distribution_prefix) = option_env!("HIERCONF_DISTRIBUTION_PREFIX") {
        add_config_path(
            Some(Utf8PathBuf::from(distribution_prefix)),
            distribution_prefix,
        );
    }

    add_config_path(Some(Utf8PathBuf::from("/usr/share/etc")), "/usr/share/etc");
    Ok(paths)
}

/// Load configuration from standard Unix hierarchy paths.
///
/// See [`load_config`](crate::load_config) for details.
pub fn load_config<T>() -> Result<T, HierConfError>
where
    T: Facet<'static>,
{
    let paths = config_paths::<T>()?;

    // Find the first existing config file
    for (path_opt, _display) in &paths {
        if let Some(path) = path_opt
            && path.exists()
            && path.is_file()
        {
            let content = fs::read_to_string(path)?;
            return Ok(facet_toml::from_str(&content)?);
        }
    }

    // No config file found, collect all locations that were checked
    let locations: Vec<Utf8PathBuf> = paths
        .into_iter()
        .filter_map(|(path_opt, _display)| path_opt)
        .collect();

    Err(HierConfError::NoConfigFiles { locations })
}