tier 0.1.17

Rust configuration library for layered TOML, env, and CLI settings
Documentation
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};

use serde::de::DeserializeOwned;
use serde::de::value::Error as ValueDeError;
use serde_json::Value;

use crate::error::{ConfigError, UnknownField};
use crate::report::ConfigReport;

use super::suggest::{merge_suggestion_paths, unknown_fields_from_paths};
use crate::loader::overrides::coerce_retry_scalars;
use crate::loader::path::normalize_external_path;

pub(in crate::loader) fn collect_unknown_fields<T>(
    value: &Value,
    suggestion_paths: &BTreeMap<String, String>,
    report: &ConfigReport,
    string_coercion_paths: &BTreeSet<String>,
) -> Result<Vec<UnknownField>, ConfigError>
where
    T: DeserializeOwned,
{
    let scan = scan_unknown_field_paths_with_retry::<T>(value, string_coercion_paths);
    scan.result.map_err(|error| ConfigError::Deserialize {
        path: "<unknown>".to_owned(),
        provenance: None,
        message: error.to_string(),
    })?;

    Ok(unknown_fields_from_paths(
        scan.ignored,
        &merge_suggestion_paths(suggestion_paths, &scan.known_paths),
        report,
    ))
}

pub(in crate::loader) fn collect_unknown_fields_best_effort<T>(
    value: &Value,
    suggestion_paths: &BTreeMap<String, String>,
    report: &ConfigReport,
    string_coercion_paths: &BTreeSet<String>,
) -> Vec<UnknownField>
where
    T: DeserializeOwned,
{
    let scan = scan_unknown_field_paths_with_retry::<T>(value, string_coercion_paths);
    unknown_fields_from_paths(
        scan.ignored,
        &merge_suggestion_paths(suggestion_paths, &scan.known_paths),
        report,
    )
}

struct UnknownFieldScan<T> {
    ignored: Vec<String>,
    known_paths: BTreeSet<String>,
    result: Result<T, ValueDeError>,
}

fn scan_unknown_field_paths<T>(
    value: &Value,
    string_coercion_paths: &BTreeSet<String>,
) -> UnknownFieldScan<T>
where
    T: DeserializeOwned,
{
    let ignored = RefCell::new(Vec::new());
    let known_paths = RefCell::new(BTreeSet::new());
    let deserializer = crate::loader::de::CoercingDeserializer::new(
        value,
        "",
        string_coercion_paths,
        Some(&known_paths),
        Some(&ignored),
    );
    let result = serde_ignored::deserialize(deserializer, |path| {
        ignored
            .borrow_mut()
            .push(normalize_external_path(&path.to_string()))
    });
    let mut ignored = ignored.into_inner();
    ignored.sort();
    ignored.dedup();
    UnknownFieldScan {
        ignored,
        known_paths: known_paths.into_inner(),
        result,
    }
}

fn scan_unknown_field_paths_with_retry<T>(
    value: &Value,
    string_coercion_paths: &BTreeSet<String>,
) -> UnknownFieldScan<T>
where
    T: DeserializeOwned,
{
    let scan = scan_unknown_field_paths::<T>(value, string_coercion_paths);
    if scan.result.is_ok() {
        return scan;
    }

    let retry_value = coerce_retry_scalars(value, "", string_coercion_paths);
    if retry_value == *value {
        return scan;
    }

    let retry_scan = scan_unknown_field_paths::<T>(&retry_value, string_coercion_paths);
    if retry_scan.result.is_ok() {
        retry_scan
    } else {
        scan
    }
}