cfgmatic-source 5.0.1

Configuration sources (file, env, memory) for cfgmatic framework
Documentation
//! Shared loading pipeline for source-backed configuration.

use std::time::Instant;

use cfgmatic_merge::{Merge, MergeBehavior, MergeOptions, TrackedLayer};

use crate::config::MergeStrategy;
use crate::domain::{Format, ParsedContent, RawContent, Result, Source, SourceError};

/// Load and parse a source through the shared parsing pipeline.
pub fn load_source_content(
    source: &dyn Source,
    default_format: Option<Format>,
) -> Result<ParsedContent> {
    source.validate()?;
    let raw = source.load_raw()?;
    parse_raw_content(&raw, source.detect_format(), default_format)
}

/// Parse raw content with detected and fallback formats.
pub fn parse_raw_content(
    raw: &RawContent,
    detected_format: Option<Format>,
    default_format: Option<Format>,
) -> Result<ParsedContent> {
    let format = normalize_format(detected_format).or(default_format);

    if let Some(format) = format {
        parse_raw_as_format(raw, format)
    } else {
        let content = raw.as_str()?;
        if let Some(format) = Format::from_content(content.as_ref()) {
            return format.parse(content.as_ref());
        }

        Err(SourceError::unsupported("cannot detect format"))
    }
}

/// Parse raw content using an explicit format.
pub fn parse_raw_as_format(raw: &RawContent, format: Format) -> Result<ParsedContent> {
    let content = raw.as_str()?;
    format.parse(content.as_ref())
}

/// Merge parsed contents with the configured merge strategy.
pub fn merge_contents(
    contents: Vec<ParsedContent>,
    strategy: MergeStrategy,
) -> Result<ParsedContent> {
    let mut iter = contents.into_iter();
    let Some(first) = iter.next() else {
        return Ok(ParsedContent::Null);
    };

    let options = merge_options(strategy);
    let mut merged = serde_json::to_value(first)
        .map_err(|error| SourceError::serialization(&error.to_string()))?;

    for next in iter {
        let next = serde_json::to_value(next)
            .map_err(|error| SourceError::serialization(&error.to_string()))?;

        merged = merged
            .merge(next, &options)
            .map_err(|error| SourceError::custom(&error.to_string()))?;
    }

    Ok(ParsedContent::from_json(merged))
}

/// Convert a source layer to a tracked merge layer.
///
/// # Errors
///
/// Returns an error if content serialization fails.
pub fn to_tracked_layer(layer: &crate::application::SourceLayer) -> Result<TrackedLayer> {
    let content = serde_json::to_value(&layer.content)
        .map_err(|error| SourceError::serialization(&error.to_string()))?;
    Ok(TrackedLayer::new(
        layer.source_id(),
        layer.priority,
        layer.registration_index,
        content,
    ))
}

fn normalize_format(format: Option<Format>) -> Option<Format> {
    format.and_then(|format| (format != Format::Unknown).then_some(format))
}

pub(super) fn merge_options(strategy: MergeStrategy) -> MergeOptions {
    let behavior = match strategy {
        MergeStrategy::Replace => MergeBehavior::Replace,
        MergeStrategy::Deep => MergeBehavior::Deep,
        MergeStrategy::Shallow => MergeBehavior::Shallow,
        MergeStrategy::Strict => MergeBehavior::Error,
    };

    MergeOptions::new()
        .behavior(behavior)
        .strict(matches!(strategy, MergeStrategy::Strict))
}

/// Convert elapsed milliseconds to `u64` without panicking on oversized durations.
#[must_use]
pub fn elapsed_millis_u64(start: Instant) -> u64 {
    u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX)
}