cfgmatic-paths 1.1.2

Cross-platform configuration path discovery following XDG and platform conventions
Documentation
//! Configuration discovery types and results.

use crate::core::config_tier::ConfigTier;
use crate::core::pattern::FilePattern;
use std::path::PathBuf;

/// Status of a configuration path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathStatus {
    /// Path exists and is a directory.
    Directory,
    /// Path exists and is a file.
    File,
    /// Path does not exist.
    NotFound,
}

impl PathStatus {
    /// Returns true if the path exists.
    #[must_use]
    pub const fn exists(self) -> bool {
        matches!(self, Self::Directory | Self::File)
    }

    /// Returns true if the path is a directory.
    #[must_use]
    pub const fn is_dir(self) -> bool {
        matches!(self, Self::Directory)
    }

    /// Returns true if the path is a file.
    #[must_use]
    pub const fn is_file(self) -> bool {
        matches!(self, Self::File)
    }
}

/// Type of configuration source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceType {
    /// Main configuration file.
    MainFile,
    /// Configuration directory containing config files.
    ConfigDir,
    /// Fragments directory (e.g., conf.d).
    FragmentsDir,
    /// Legacy configuration file (e.g., ~/.apprc).
    Legacy,
}

/// Information about a configuration path candidate.
#[derive(Debug, Clone)]
pub struct ConfigCandidate {
    /// Full path to the configuration.
    pub path: PathBuf,
    /// Existence status of the path.
    pub status: PathStatus,
    /// Priority tier of this configuration.
    pub tier: ConfigTier,
    /// Type of configuration source.
    pub source_type: SourceType,
}

impl ConfigCandidate {
    /// Create a new config candidate.
    #[must_use]
    pub const fn new(
        path: PathBuf,
        status: PathStatus,
        tier: ConfigTier,
        source_type: SourceType,
    ) -> Self {
        Self {
            path,
            status,
            tier,
            source_type,
        }
    }

    /// Returns true if this candidate exists.
    #[must_use]
    pub const fn exists(&self) -> bool {
        self.status.exists()
    }

    /// Returns true if this candidate is a file.
    #[must_use]
    pub const fn is_file(&self) -> bool {
        self.status.is_file()
    }

    /// Returns true if this candidate is a directory.
    #[must_use]
    pub const fn is_dir(&self) -> bool {
        self.status.is_dir()
    }
}

/// Result of a configuration discovery operation.
///
/// Provides comprehensive information about configuration locations,
/// including the preferred path for new configurations and all
/// candidates that were searched.
#[derive(Debug, Clone)]
pub struct ConfigDiscovery {
    /// Preferred path for creating a new configuration (highest priority user path).
    pub preferred_path: PathBuf,
    /// Path to the first found existing configuration, if any.
    pub found_path: Option<PathBuf>,
    /// All candidates searched in priority order.
    pub candidates: Vec<ConfigCandidate>,
    /// Found configuration fragments (from conf.d and similar directories).
    pub fragments: Vec<PathBuf>,
}

impl ConfigDiscovery {
    /// Create a new config discovery result.
    #[must_use]
    pub const fn new(
        preferred_path: PathBuf,
        found_path: Option<PathBuf>,
        candidates: Vec<ConfigCandidate>,
        fragments: Vec<PathBuf>,
    ) -> Self {
        Self {
            preferred_path,
            found_path,
            candidates,
            fragments,
        }
    }

    /// Returns true if any configuration was found.
    #[must_use]
    pub const fn has_config(&self) -> bool {
        self.found_path.is_some()
    }

    /// Get all found configuration files (not directories).
    #[must_use]
    pub fn found_files(&self) -> Vec<&ConfigCandidate> {
        self.candidates.iter().filter(|c| c.is_file()).collect()
    }

    /// Get all found configuration directories.
    #[must_use]
    pub fn found_dirs(&self) -> Vec<&ConfigCandidate> {
        self.candidates.iter().filter(|c| c.is_dir()).collect()
    }

    /// Get candidates by tier.
    #[must_use]
    pub fn by_tier(&self, tier: ConfigTier) -> Vec<&ConfigCandidate> {
        self.candidates.iter().filter(|c| c.tier == tier).collect()
    }

    /// Get candidates by source type.
    #[must_use]
    pub fn by_source_type(&self, source_type: SourceType) -> Vec<&ConfigCandidate> {
        self.candidates
            .iter()
            .filter(|c| c.source_type == source_type)
            .collect()
    }

    /// Create an empty discovery result.
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            preferred_path: PathBuf::new(),
            found_path: None,
            candidates: Vec::new(),
            fragments: Vec::new(),
        }
    }
}

impl Default for ConfigDiscovery {
    fn default() -> Self {
        Self::empty()
    }
}

/// Options for configuration file discovery.
#[derive(Debug, Clone)]
pub struct DiscoveryOptions {
    /// File pattern to match.
    pub pattern: FilePattern,
    /// Whether to search for fragments (conf.d style).
    pub include_fragments: bool,
    /// Fragment directory name (e.g., "conf.d").
    pub fragment_dir: String,
    /// Whether to include legacy paths.
    pub include_legacy: bool,
}

impl DiscoveryOptions {
    /// Create new discovery options.
    #[must_use]
    pub fn new() -> Self {
        Self {
            pattern: FilePattern::default(),
            include_fragments: true,
            fragment_dir: String::from("conf.d"),
            include_legacy: true,
        }
    }

    /// Set the file pattern.
    #[must_use]
    pub fn with_pattern(mut self, pattern: FilePattern) -> Self {
        self.pattern = pattern;
        self
    }

    /// Enable or disable fragment discovery.
    #[must_use]
    pub const fn with_fragments(mut self, include: bool) -> Self {
        self.include_fragments = include;
        self
    }

    /// Set the fragment directory name.
    #[must_use]
    pub fn with_fragment_dir(mut self, dir: impl Into<String>) -> Self {
        self.fragment_dir = dir.into();
        self
    }

    /// Enable or disable legacy paths.
    #[must_use]
    pub const fn with_legacy(mut self, include: bool) -> Self {
        self.include_legacy = include;
        self
    }
}

impl Default for DiscoveryOptions {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_path_status() {
        assert!(PathStatus::Directory.exists());
        assert!(PathStatus::File.exists());
        assert!(!PathStatus::NotFound.exists());
        assert!(PathStatus::Directory.is_dir());
        assert!(PathStatus::File.is_file());
    }

    #[test]
    fn test_config_candidate() {
        let candidate = ConfigCandidate::new(
            PathBuf::from("/test/config"),
            PathStatus::File,
            ConfigTier::User,
            SourceType::MainFile,
        );
        assert!(candidate.exists());
        assert!(candidate.is_file());
        assert!(!candidate.is_dir());
    }

    #[test]
    fn test_config_discovery_empty() {
        let discovery = ConfigDiscovery::empty();
        assert!(!discovery.has_config());
        assert!(discovery.found_path.is_none());
        assert!(discovery.candidates.is_empty());
        assert!(discovery.fragments.is_empty());
    }

    #[test]
    fn test_discovery_options_default() {
        let options = DiscoveryOptions::new();
        assert!(options.include_fragments);
        assert!(options.include_legacy);
        assert_eq!(options.fragment_dir, "conf.d");
    }

    #[test]
    fn test_discovery_options_builder() {
        let options = DiscoveryOptions::new()
            .with_pattern(FilePattern::exact("myapp.toml"))
            .with_fragments(false)
            .with_fragment_dir("fragments")
            .with_legacy(false);

        assert!(!options.include_fragments);
        assert!(!options.include_legacy);
        assert_eq!(options.fragment_dir, "fragments");
    }
}