cfgmatic-source 5.0.1

Configuration sources (file, env, memory) for cfgmatic framework
Documentation
//! Builder types for [`CascadeLoader`](super::CascadeLoader).

use std::path::PathBuf;

use crate::config::{ErrorMode, LoadOptions, MergeStrategy};
use crate::domain::{Format, Result, SourceError};

use super::{CascadeLayer, CascadeLoader, CascadeScope};

/// Builder for [`CascadeLayer`].
#[derive(Debug, Clone, Default)]
pub struct CascadeLayerBuilder {
    scope: Option<CascadeScope>,
    path: Option<PathBuf>,
    optional: bool,
    priority: i32,
    format: Option<Format>,
}

impl CascadeLayerBuilder {
    /// Create a new builder.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the logical scope.
    #[must_use]
    pub fn scope(mut self, scope: CascadeScope) -> Self {
        self.scope = Some(scope);
        self
    }

    /// Set the file path.
    #[must_use]
    pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
        self.path = Some(path.into());
        self
    }

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

    /// Set the 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
    }

    /// Build the layer definition.
    ///
    /// # Errors
    ///
    /// Returns an error when either scope or path is missing.
    pub fn build(self) -> Result<CascadeLayer> {
        let scope = self
            .scope
            .ok_or_else(|| SourceError::validation("Cascade layer scope is required"))?;
        let path = self
            .path
            .ok_or_else(|| SourceError::validation("Cascade layer path is required"))?;

        Ok(CascadeLayer {
            scope,
            path,
            optional: self.optional,
            priority: self.priority,
            format: self.format,
        })
    }
}

/// Builder for [`CascadeLoader`].
#[derive(Debug, Clone, Default)]
pub struct CascadeLoaderBuilder {
    layers: Vec<CascadeLayer>,
    options: Option<LoadOptions>,
    default_format: Option<Format>,
}

impl CascadeLoaderBuilder {
    /// Create a new builder.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a fully defined layer.
    #[must_use]
    pub fn add_layer(mut self, layer: CascadeLayer) -> Self {
        self.layers.push(layer);
        self
    }

    /// Add a required file layer.
    #[must_use]
    pub fn add_file(mut self, scope: CascadeScope, path: impl Into<PathBuf>) -> Self {
        self.layers.push(CascadeLayer::new(scope, path));
        self
    }

    /// Add an optional file layer.
    #[must_use]
    pub fn add_optional_file(mut self, scope: CascadeScope, path: impl Into<PathBuf>) -> Self {
        self.layers
            .push(CascadeLayer::new(scope, path).optional(true));
        self
    }

    /// Add the standard system/local/user layers discovered for an application.
    #[cfg(feature = "file")]
    #[must_use]
    pub fn discover_app(mut self, app_name: impl AsRef<str>) -> Self {
        let options = self.options.clone().unwrap_or_default();
        let extensions: Vec<&str> = options.extensions.iter().map(String::as_str).collect();
        let pattern = cfgmatic_paths::FilePattern::extensions(options.base_name, &extensions);

        let finder = cfgmatic_paths::PathsBuilder::new(app_name.as_ref()).build();
        for candidate in finder
            .find_config_files(&pattern)
            .into_iter()
            .filter(cfgmatic_paths::ConfigCandidate::is_file)
        {
            let scope = match candidate.tier {
                cfgmatic_paths::ConfigTier::System => CascadeScope::System,
                cfgmatic_paths::ConfigTier::Local => CascadeScope::Local,
                cfgmatic_paths::ConfigTier::User => CascadeScope::User,
            };

            self.layers.push(
                CascadeLayer::new(scope, candidate.path)
                    .priority(i32::from(u8::from(candidate.tier))),
            );
        }

        self
    }

    /// Set load options.
    #[must_use]
    pub fn options(mut self, options: LoadOptions) -> Self {
        self.options = Some(options);
        self
    }

    /// Set merge strategy.
    #[must_use]
    pub fn merge_strategy(mut self, merge_strategy: MergeStrategy) -> Self {
        let mut options = self.options.unwrap_or_default();
        options.merge_strategy = merge_strategy;
        self.options = Some(options);
        self
    }

    /// Set fail-fast behavior.
    #[must_use]
    pub fn fail_fast(mut self, fail_fast: bool) -> Self {
        let mut options = self.options.unwrap_or_default();
        options.error_mode = ErrorMode::from_fail_fast(fail_fast);
        self.options = Some(options);
        self
    }

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

    /// Build the cascade loader.
    #[must_use]
    pub fn build(self) -> CascadeLoader {
        CascadeLoader {
            layers: self.layers,
            options: self.options.unwrap_or_default(),
            default_format: self.default_format,
        }
    }
}