cfgmatic-paths 0.1.3

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Cross-platform configuration path discovery following XDG and platform conventions.
//!
//! This crate provides a platform-agnostic way to discover configuration
//! directories following the conventions of each operating system:
//!
//! - **Unix/Linux**: XDG Base Directory Specification
//! - **macOS CLI**: XDG (same as Unix)
//! - **macOS GUI**: Application Support directories
//! - **Windows**: Known Folder IDs (`AppData`, `ProgramData`)
//!
//! # Quick Start
//!
//! ```
//! use cfgmatic_paths::PathsBuilder;
//!
//! // Create a path finder for your application
//! let finder = PathsBuilder::new("myapp").build();
//!
//! // Get user config directories
//! let user_dirs = finder.user_dirs();
//! println!("User config dirs: {:?}", user_dirs);
//!
//! // Ensure the primary user config directory exists
//! if let Ok(config_dir) = finder.ensure_user_config_dir() {
//!     println!("Config dir: {}", config_dir.display());
//! }
//! ```
//!
//! # Configuration Tiers
//!
//! Configuration directories are organized into three tiers:
//!
//! 1. **User** (`ConfigTier::User`): User-specific configs in home directory.
//!    Highest priority, typically writable.
//!
//! 2. **Local** (`ConfigTier::Local`): Machine-specific configs.
//!    Medium priority, may be writable.
//!
//! 3. **System** (`ConfigTier::System`): System-wide configs.
//!    Lowest priority, typically read-only.
//!
//! # Platform Details
//!
//! ## Unix/Linux (XDG)
//!
//! Uses the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html):
//!
//! - User: `$XDG_CONFIG_HOME/<app>/` (default: `~/.config/<app>/`)
//! - Legacy: `~/.<app>rc` (optional, enabled by default)
//! - System: `$XDG_CONFIG_DIRS/<app>/` (default: `/etc/xdg/<app>/`)
//!
//! ## macOS
//!
//! ### CLI Applications
//! Uses XDG (same as Unix/Linux).
//!
//! ### GUI Applications (with `macos-gui` feature)
//! Uses macOS conventions:
//!
//! - User: `~/Library/Application Support/<bundle-id>/`
//! - System: `/Library/Application Support/<bundle-id>/`
//!
//! ## Windows
//!
//! Uses Windows Known Folder IDs:
//!
//! - User Roaming: `%APPDATA%\<Company>\<App>\`
//! - User Local: `%LOCALAPPDATA%\<Company>\<App>\`
//! - System: `%PROGRAMDATA%\<Company>\<App>\`
//!
//! # Testing
//!
//! The crate provides abstractions for testing:
//!
//! - [`Env`]: Mock environment variables
//! - [`Fs`]: Mock filesystem operations
//!
//! These allow testing without modifying the actual environment or filesystem.

#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]

pub use builder::{PathFinder, PathsBuilder};
pub use core::{AppType, ConfigTier, FilePattern};
pub use env::{Env, StdEnv};
pub use filesystem::{Fs, StdFs};
pub use platform::{DirectoryFinder, DirectoryInfo};

mod builder;
mod core;
mod env;
mod filesystem;
mod platform;

/// Platform-specific directory finder implementations.
pub mod finders {

    cfg_if::cfg_if! {
        if #[cfg(all(target_os = "macos", feature = "macos-gui"))] {
            pub use crate::platform::MacOSGuiDirectoryFinder;
        } else if #[cfg(windows)] {
            pub use crate::platform::WindowsDirectoryFinder;
        } else {
            pub use crate::platform::UnixDirectoryFinder;
        }
    }
}

/// Find the first existing configuration directory.
///
/// Searches in order: User → Local → System.
/// Returns the first directory that exists, or `None`.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::find_existing_dir;
///
/// if let Some(dir) = find_existing_dir("myapp") {
///     println!("Found config dir: {}", dir.display());
/// }
/// ```
pub fn find_existing_dir(app_name: impl AsRef<str>) -> Option<std::path::PathBuf> {
    let finder = PathsBuilder::new(app_name.as_ref()).build();

    // Search in priority order: User → Local → System
    finder
        .user_dirs()
        .into_iter()
        .chain(finder.local_dirs())
        .chain(finder.system_dirs())
        .find(|dir| dir.exists())
}

/// Find or create the user configuration directory.
///
/// Returns the primary user config directory, creating it if it doesn't exist.
///
/// # Errors
///
/// Returns an error if the directory cannot be created.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::ensure_config_dir;
///
/// match ensure_config_dir("myapp") {
///     Ok(dir) => println!("Config dir: {}", dir.display()),
///     Err(e) => eprintln!("Failed to create config dir: {}", e),
/// }
/// ```
pub fn ensure_config_dir(app_name: impl AsRef<str>) -> std::io::Result<std::path::PathBuf> {
    PathsBuilder::new(app_name.as_ref())
        .build()
        .ensure_user_config_dir()
}

/// Get all configuration directories with their metadata.
///
/// Returns a list of all known configuration directories with their
/// tier and existence status.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::get_all_dirs;
///
/// for info in get_all_dirs("myapp") {
///     println!("{:?}: {} (exists: {})",
///         info.tier, info.path.display(), info.exists);
/// }
/// ```
pub fn get_all_dirs(app_name: impl AsRef<str>) -> Vec<DirectoryInfo> {
    PathsBuilder::new(app_name.as_ref()).build().all_dirs()
}

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

    #[test]
    fn test_exports() {
        // Just verify that all public types are accessible
        let _: PathsBuilder = PathsBuilder::new("test");
        let _: AppType = AppType::Cli;
        let _: ConfigTier = ConfigTier::User;
        let _: FilePattern = FilePattern::default();
    }

    #[test]
    fn test_ensure_config_dir_creates_dir() {
        // This test is limited - we can't easily test directory creation
        // without a mock filesystem, but we can verify the API works
        let result = ensure_config_dir("test_app_that_probably_does_not_exist_12345");
        // The result depends on the environment, so we just verify it compiles/runs
        let _ = result;
    }
}