cloudiful-config 0.4.1

Small serde-based config helpers for TOML, JSON, JSONC, env overrides, and atomic saves.
Documentation
use std::io::{self, ErrorKind};

fn parse_env_value(raw: &str) -> serde_json::Value {
    serde_json::from_str(raw).unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
}

fn insert_env_value(
    root: &mut serde_json::Map<String, serde_json::Value>,
    key: &str,
    value: serde_json::Value,
) {
    let mut segments = key
        .split("__")
        .filter(|segment| !segment.is_empty())
        .map(|segment| segment.to_ascii_lowercase())
        .peekable();

    if segments.peek().is_none() {
        return;
    }

    let mut current = root;
    while let Some(segment) = segments.next() {
        if segments.peek().is_none() {
            current.insert(segment, value);
            return;
        }

        let entry = current
            .entry(segment)
            .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));

        if !entry.is_object() {
            *entry = serde_json::Value::Object(serde_json::Map::new());
        }

        current = entry.as_object_mut().expect("entry is forced to be object");
    }
}

fn env_overrides(prefix: &str) -> serde_json::Value {
    let mut root = serde_json::Map::new();

    for (key, value) in std::env::vars() {
        let Some(stripped_key) = key.strip_prefix(prefix) else {
            continue;
        };

        insert_env_value(&mut root, stripped_key, parse_env_value(&value));
    }

    serde_json::Value::Object(root)
}

fn merge_json(base: &mut serde_json::Value, overrides: serde_json::Value) {
    match (base, overrides) {
        (serde_json::Value::Object(base_map), serde_json::Value::Object(override_map)) => {
            for (key, override_value) in override_map {
                match base_map.get_mut(&key) {
                    Some(base_value) => merge_json(base_value, override_value),
                    None => {
                        base_map.insert(key, override_value);
                    }
                }
            }
        }
        (base_value, override_value) => {
            *base_value = override_value;
        }
    }
}

pub(crate) fn apply_env_overrides<T>(config: T, prefix: &str) -> Result<T, io::Error>
where
    T: serde::Serialize + serde::de::DeserializeOwned,
{
    let mut base = serde_json::to_value(config).map_err(|e| {
        io::Error::new(
            ErrorKind::InvalidData,
            format!(
                "failed to serialize config before applying environment overrides for prefix {prefix}: {e}"
            ),
        )
    })?;
    let overrides = env_overrides(prefix);

    merge_json(&mut base, overrides);

    serde_json::from_value(base).map_err(|e| {
        io::Error::new(
            ErrorKind::InvalidData,
            format!(
                "failed to deserialize config after applying environment overrides for prefix {prefix}: {e}"
            ),
        )
    })
}