cfgmatic-paths 0.1.6

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, ConfigCandidate, ConfigDiscovery, ConfigFileRule, ConfigRuleSet, ConfigTier,
    DiscoveryOptions, FilePattern, FragmentRule, PathStatus, RuleBasedDiscovery, RuleMatchResult,
    SourceType, TierIterator, TierSearchMode,
};
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> {
    PathsBuilder::new(app_name.as_ref())
        .build()
        .config_directories()
        .into_iter()
        .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()
}

/// Discover configuration with full diagnostics.
///
/// This is a convenience function that creates a finder and returns
/// comprehensive information about configuration locations.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::discover_config;
///
/// let discovery = discover_config("myapp");
/// println!("Preferred: {}", discovery.preferred_path.display());
///
/// for candidate in discovery.candidates {
///     println!("  - {:?}: {} ({:?})",
///         candidate.tier,
///         candidate.path.display(),
///         candidate.status
///     );
/// }
/// ```
pub fn discover_config(app_name: impl AsRef<str>) -> ConfigDiscovery {
    PathsBuilder::new(app_name.as_ref())
        .build()
        .discover_config()
}

/// Get the preferred configuration path (without checking existence).
///
/// Returns the first user config directory path, regardless of whether
/// it exists. This is useful for determining where to create a new
/// configuration file.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::config_path;
///
/// let path = config_path("myapp");
/// println!("Config would be at: {}", path.display());
/// ```
pub fn config_path(app_name: impl AsRef<str>) -> std::path::PathBuf {
    PathsBuilder::new(app_name.as_ref())
        .build()
        .preferred_config_path()
}

/// Get the preferred configuration file path.
///
/// Returns the full path to a config file in the preferred location,
/// without checking existence.
///
/// # Example
///
/// ```
/// use cfgmatic_paths::config_file_path;
///
/// let path = config_file_path("myapp", "config.toml");
/// println!("Config file would be at: {}", path.display());
/// ```
pub fn config_file_path(
    app_name: impl AsRef<str>,
    filename: impl AsRef<std::path::Path>,
) -> std::path::PathBuf {
    PathsBuilder::new(app_name.as_ref())
        .build()
        .preferred_config_file(filename)
}