cfgmatic-source 5.0.1

Configuration sources (file, env, memory) for cfgmatic framework
Documentation
//! Git-like cascading configuration loading.
//!
//! A cascade is an ordered stack of configuration files where later layers
//! override earlier layers. This models setups such as system -> user ->
//! repository -> worktree configuration.

mod builder;
mod service;

use std::fmt;
use std::path::PathBuf;

use serde::de::DeserializeOwned;

use crate::config::LoadOptions;
use crate::domain::{Format, ParsedContent, Result};
pub use builder::{CascadeLayerBuilder, CascadeLoaderBuilder};

/// A logical scope in a cascading configuration stack.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CascadeScope {
    /// System-wide configuration, typically in `/etc`.
    System,
    /// User or global configuration, typically in the home directory.
    Global,
    /// App-specific user configuration.
    User,
    /// Repository-local or machine-local configuration.
    Local,
    /// Worktree-specific configuration layered above repository config.
    Worktree,
    /// Any custom scope name.
    Custom(String),
}

impl fmt::Display for CascadeScope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::System => write!(f, "system"),
            Self::Global => write!(f, "global"),
            Self::User => write!(f, "user"),
            Self::Local => write!(f, "local"),
            Self::Worktree => write!(f, "worktree"),
            Self::Custom(scope) => write!(f, "{scope}"),
        }
    }
}

/// A single file layer in a cascade.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CascadeLayer {
    /// Logical scope of the layer.
    pub scope: CascadeScope,
    /// Backing file path.
    pub path: PathBuf,
    /// Whether the layer may be absent.
    pub optional: bool,
    /// Priority used when multiple layers of the same scope are present.
    pub priority: i32,
    /// Explicit format override for extensionless files.
    pub format: Option<Format>,
}

impl CascadeLayer {
    /// Create a new file layer.
    #[must_use]
    pub fn new(scope: CascadeScope, path: impl Into<PathBuf>) -> Self {
        Self {
            priority: 0,
            scope,
            path: path.into(),
            optional: false,
            format: None,
        }
    }

    /// Create a builder for a file layer.
    #[must_use]
    pub fn builder() -> CascadeLayerBuilder {
        CascadeLayerBuilder::new()
    }

    /// Mark the layer optional.
    #[must_use]
    pub const fn optional(mut self, optional: bool) -> Self {
        self.optional = optional;
        self
    }

    /// Set the layer priority.
    #[must_use]
    pub const fn priority(mut self, priority: i32) -> Self {
        self.priority = priority;
        self
    }

    /// Set a format override.
    #[must_use]
    pub const fn format(mut self, format: Format) -> Self {
        self.format = Some(format);
        self
    }
}

/// Diagnostic information about a resolved layer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedCascadeLayer {
    /// Logical scope.
    pub scope: CascadeScope,
    /// Backing file path.
    pub path: PathBuf,
    /// Merge priority.
    pub priority: i32,
}

impl From<&CascadeLayer> for ResolvedCascadeLayer {
    fn from(layer: &CascadeLayer) -> Self {
        Self {
            scope: layer.scope.clone(),
            path: layer.path.clone(),
            priority: layer.priority,
        }
    }
}

/// Result of loading a cascade.
#[derive(Debug, Clone)]
pub struct CascadeLoadResult {
    /// Merged configuration content.
    pub content: ParsedContent,
    /// Layers that contributed content.
    pub loaded_layers: Vec<ResolvedCascadeLayer>,
    /// Optional layers skipped because the file was absent.
    pub skipped_layers: Vec<ResolvedCascadeLayer>,
    /// Non-fatal layer failures collected when `fail_fast` is disabled.
    pub failed_layers: Vec<(ResolvedCascadeLayer, String)>,
    /// End-to-end load time in milliseconds.
    pub processing_time_ms: u64,
}

impl CascadeLoadResult {
    /// Get merged content.
    #[must_use]
    pub const fn content(&self) -> &ParsedContent {
        &self.content
    }

    /// Convert the merged content into a typed configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if deserialization fails.
    pub fn to_type<T: DeserializeOwned>(&self) -> Result<T> {
        self.content.to_type()
    }
}

/// Loader for cascading file-based configuration.
#[derive(Debug, Clone, Default)]
pub struct CascadeLoader {
    pub(super) layers: Vec<CascadeLayer>,
    pub(super) options: LoadOptions,
    pub(super) default_format: Option<Format>,
}

impl CascadeLoader {
    /// Create an empty cascade loader.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a builder for a cascade loader.
    #[must_use]
    pub fn builder() -> CascadeLoaderBuilder {
        CascadeLoaderBuilder::new()
    }
}

#[cfg(test)]
mod tests;