tier 0.1.12

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use super::*;

pub(super) struct ParsedOverride {
    pub(super) value: Value,
    pub(super) string_coercion_suffixes: BTreeSet<String>,
}

pub(super) fn parse_override_value(raw: &str) -> Result<ParsedOverride, String> {
    if raw.is_empty() {
        return Ok(ParsedOverride {
            value: Value::String(String::new()),
            string_coercion_suffixes: BTreeSet::from([String::new()]),
        });
    }

    let trimmed = raw.trim();

    let uses_explicit_json_syntax =
        matches!(trimmed.chars().next(), Some('{') | Some('[') | Some('"'));

    if uses_explicit_json_syntax {
        let value = serde_json::from_str::<Value>(trimmed)
            .map_err(|error| format!("invalid explicit JSON override: {error}"))?;
        return Ok(ParsedOverride {
            value,
            string_coercion_suffixes: BTreeSet::new(),
        });
    }

    Ok(ParsedOverride {
        value: Value::String(raw.to_owned()),
        string_coercion_suffixes: BTreeSet::from([String::new()]),
    })
}

pub(super) fn parse_env_override_value(
    raw: &str,
    decoder: Option<EnvDecoder>,
    custom_decoder: Option<&CustomEnvDecoder>,
) -> Result<ParsedOverride, String> {
    match (custom_decoder, decoder) {
        (Some(custom_decoder), _) => {
            let value = custom_decoder(raw)?;
            Ok(ParsedOverride {
                string_coercion_suffixes: collect_string_leaf_suffixes(&value, ""),
                value,
            })
        }
        (None, Some(decoder)) => {
            let value = decode_env_override_value(raw, decoder)?;
            Ok(ParsedOverride {
                string_coercion_suffixes: collect_string_leaf_suffixes(&value, ""),
                value,
            })
        }
        (None, None) => parse_override_value(raw),
    }
}

fn decode_env_override_value(raw: &str, decoder: EnvDecoder) -> Result<Value, String> {
    match decoder {
        EnvDecoder::Csv => Ok(Value::Array(
            raw.split(',')
                .map(str::trim)
                .filter(|segment| !segment.is_empty())
                .map(|segment| Value::String(segment.to_owned()))
                .collect(),
        )),
        EnvDecoder::Whitespace => Ok(Value::Array(
            raw.split_whitespace()
                .map(|segment| Value::String(segment.to_owned()))
                .collect(),
        )),
        EnvDecoder::PathList => {
            let values = std::env::split_paths(OsStr::new(raw))
                .map(|path| Value::String(path.to_string_lossy().into_owned()))
                .collect();
            Ok(Value::Array(values))
        }
        EnvDecoder::KeyValueMap => {
            let mut map = Map::new();
            for entry in raw
                .split(',')
                .map(str::trim)
                .filter(|entry| !entry.is_empty())
            {
                let (key, value) = entry.split_once('=').ok_or_else(|| {
                    format!("invalid key_value_map entry `{entry}`, expected key=value")
                })?;
                let key = key.trim();
                let value = value.trim();
                if key.is_empty() {
                    return Err("key_value_map entries must not use an empty key".to_owned());
                }
                map.insert(key.to_owned(), Value::String(value.to_owned()));
            }
            Ok(Value::Object(map))
        }
    }
}

pub(super) fn collect_string_leaf_suffixes(value: &Value, prefix: &str) -> BTreeSet<String> {
    let mut suffixes = BTreeSet::new();
    collect_string_leaf_suffixes_inner(value, prefix, &mut suffixes);
    suffixes
}

fn collect_string_leaf_suffixes_inner(
    value: &Value,
    prefix: &str,
    suffixes: &mut BTreeSet<String>,
) {
    match value {
        Value::String(_) => {
            suffixes.insert(prefix.to_owned());
        }
        Value::Array(values) => {
            for (index, value) in values.iter().enumerate() {
                let next = join_path(prefix, &index.to_string());
                collect_string_leaf_suffixes_inner(value, &next, suffixes);
            }
        }
        Value::Object(map) => {
            for (key, value) in map {
                let next = join_path(prefix, key);
                collect_string_leaf_suffixes_inner(value, &next, suffixes);
            }
        }
        _ => {}
    }
}

pub(super) fn coerce_retry_scalars(
    value: &Value,
    current_path: &str,
    string_coercion_paths: &BTreeSet<String>,
) -> Value {
    match value {
        Value::Object(map) => Value::Object(
            map.iter()
                .map(|(key, child)| {
                    let next = join_path(current_path, key);
                    (
                        key.clone(),
                        coerce_retry_scalars(child, &next, string_coercion_paths),
                    )
                })
                .collect(),
        ),
        Value::Array(values) => Value::Array(
            values
                .iter()
                .enumerate()
                .map(|(index, child)| {
                    let next = join_path(current_path, &index.to_string());
                    coerce_retry_scalars(child, &next, string_coercion_paths)
                })
                .collect(),
        ),
        Value::String(raw) if string_coercion_paths.contains(current_path) => {
            retry_scalar_value(raw).unwrap_or_else(|| Value::String(raw.clone()))
        }
        other => other.clone(),
    }
}

fn retry_scalar_value(raw: &str) -> Option<Value> {
    let value = serde_json::from_str::<Value>(raw.trim()).ok()?;
    match value {
        Value::Null | Value::Bool(_) | Value::Number(_) => Some(value),
        _ => None,
    }
}