use std::fmt;
use crate::error::{Error, Origin};
use crate::snapshot::Snapshot;
use crate::source::LoadSpec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownKey {
pub path: String,
pub suggestion: Option<String>,
}
impl fmt::Display for UnknownKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: unknown key", self.path)?;
if let Some(suggestion) = &self.suggestion {
write!(f, ", did you mean `{suggestion}`?")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub path: String,
pub origin: Origin,
}
impl fmt::Display for Resolved {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:<28} {}", self.path, self.origin)
}
}
#[derive(Debug, Clone)]
pub struct Report {
pub key: String,
pub resolved: Vec<Resolved>,
pub unknown: Vec<UnknownKey>,
pub failure: Option<String>,
}
impl Report {
#[must_use]
pub fn is_clean(&self) -> bool {
self.failure.is_none() && self.unknown.is_empty()
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "[{}]", self.key)?;
if self.resolved.is_empty() {
writeln!(f, " (nothing supplies this section)")?;
}
for resolved in &self.resolved {
writeln!(f, " {resolved}")?;
}
if !self.unknown.is_empty() {
writeln!(f)?;
for unknown in &self.unknown {
writeln!(f, " {unknown}")?;
}
}
match &self.failure {
Some(failure) => write!(f, "\n would not load: {failure}"),
None => write!(f, "\n would load"),
}
}
}
pub fn check<T>(spec: &LoadSpec<'_>, fields: &[&str]) -> Result<Report, Error>
where
T: serde::de::DeserializeOwned,
{
let (snapshot, figment) = crate::loader::resolved(spec)?;
let paths = snapshot.leaf_paths();
let resolved = paths
.iter()
.map(|path| Resolved {
path: path.clone(),
origin: crate::loader::origin_in(&figment, path),
})
.collect();
Ok(Report {
key: spec.key.to_owned(),
resolved,
unknown: unknown_keys(&snapshot, fields, &aliased_keys(spec)),
failure: crate::loader::load::<T>(spec)
.err()
.map(|error| error.to_string()),
})
}
fn aliased_keys(spec: &LoadSpec<'_>) -> Vec<String> {
spec.aliases
.map(crate::Aliases::known_keys)
.unwrap_or_default()
}
fn unknown_keys(snapshot: &Snapshot, fields: &[&str], aliased: &[String]) -> Vec<UnknownKey> {
if fields.is_empty() {
return Vec::new();
}
snapshot
.top_level_keys()
.into_iter()
.filter(|key| !fields.contains(&key.as_str()))
.filter(|key| !aliased.iter().any(|alias| alias == key))
.map(|key| UnknownKey {
suggestion: closest(&key, fields),
path: key,
})
.collect()
}
fn closest(key: &str, fields: &[&str]) -> Option<String> {
let budget = (key.len() / 4).max(1);
fields
.iter()
.map(|field| (distance(key, field), *field))
.filter(|(distance, _)| *distance <= budget)
.min_by_key(|(distance, _)| *distance)
.map(|(_, field)| field.to_owned())
}
fn distance(left: &str, right: &str) -> usize {
let left: Vec<char> = left.chars().collect();
let right: Vec<char> = right.chars().collect();
let mut before_last = vec![0; right.len() + 1];
let mut previous: Vec<usize> = (0..=right.len()).collect();
let mut current = vec![0; right.len() + 1];
for i in 0..left.len() {
current[0] = i + 1;
for j in 0..right.len() {
let substitution = usize::from(left[i] != right[j]);
current[j + 1] = (previous[j] + substitution)
.min(previous[j + 1] + 1)
.min(current[j] + 1);
if i > 0 && j > 0 && left[i] == right[j - 1] && left[i - 1] == right[j] {
current[j + 1] = current[j + 1].min(before_last[j - 1] + 1);
}
}
std::mem::swap(&mut before_last, &mut previous);
std::mem::swap(&mut previous, &mut current);
}
previous[right.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_close_miss_is_suggested() {
assert_eq!(closest("hsot", &["host", "port"]).as_deref(), Some("host"));
assert_eq!(closest("prot", &["host", "port"]).as_deref(), Some("port"));
assert_eq!(closest("hosts", &["host", "port"]).as_deref(), Some("host"));
}
#[test]
fn an_unrelated_key_suggests_nothing() {
assert_eq!(closest("elephant", &["host", "port"]), None);
}
#[test]
fn the_budget_scales_with_the_name() {
assert_eq!(closest("od", &["id"]).as_deref(), Some("id"));
assert_eq!(
closest("conection_timout", &["connection_timeout"]).as_deref(),
Some("connection_timeout")
);
assert_eq!(closest("xy", &["id"]), None);
}
#[test]
fn distance_counts_edits() {
assert_eq!(distance("", ""), 0);
assert_eq!(distance("host", "host"), 0);
assert_eq!(distance("host", ""), 4);
assert_eq!(distance("port", "sport"), 1, "one insertion");
assert_eq!(distance("port", "pxrt"), 1, "one substitution");
}
#[test]
fn a_transposition_is_one_edit_not_two() {
assert_eq!(distance("port", "prot"), 1);
assert_eq!(distance("host", "hsot"), 1);
}
#[test]
fn an_unknown_key_renders_its_suggestion() {
let unknown = UnknownKey {
path: "hsot".to_owned(),
suggestion: Some("host".to_owned()),
};
assert_eq!(
unknown.to_string(),
"hsot: unknown key, did you mean `host`?"
);
}
}