1use std::fmt;
29
30use crate::error::{Error, Origin};
31use crate::snapshot::Snapshot;
32use crate::source::LoadSpec;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct UnknownKey {
37 pub path: String,
39 pub suggestion: Option<String>,
41}
42
43impl fmt::Display for UnknownKey {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "{}: unknown key", self.path)?;
46
47 if let Some(suggestion) = &self.suggestion {
48 write!(f, ", did you mean `{suggestion}`?")?;
49 }
50
51 Ok(())
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Resolved {
58 pub path: String,
60 pub origin: Origin,
62}
63
64impl fmt::Display for Resolved {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "{:<28} {}", self.path, self.origin)
67 }
68}
69
70#[derive(Debug, Clone)]
76pub struct Report {
77 pub key: String,
79 pub resolved: Vec<Resolved>,
81 pub unknown: Vec<UnknownKey>,
83 pub failure: Option<String>,
85}
86
87impl Report {
88 #[must_use]
90 pub fn is_clean(&self) -> bool {
91 self.failure.is_none() && self.unknown.is_empty()
92 }
93}
94
95impl fmt::Display for Report {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 writeln!(f, "[{}]", self.key)?;
98
99 if self.resolved.is_empty() {
100 writeln!(f, " (nothing supplies this section)")?;
101 }
102
103 for resolved in &self.resolved {
104 writeln!(f, " {resolved}")?;
105 }
106
107 if !self.unknown.is_empty() {
108 writeln!(f)?;
109
110 for unknown in &self.unknown {
111 writeln!(f, " {unknown}")?;
112 }
113 }
114
115 match &self.failure {
116 Some(failure) => write!(f, "\n would not load: {failure}"),
117 None => write!(f, "\n would load"),
118 }
119 }
120}
121
122pub fn check<T>(spec: &LoadSpec<'_>, fields: &[&str]) -> Result<Report, Error>
133where
134 T: serde::de::DeserializeOwned,
135{
136 let (snapshot, figment) = crate::loader::resolved(spec)?;
140 let paths = snapshot.leaf_paths();
141
142 let resolved = paths
143 .iter()
144 .map(|path| Resolved {
145 path: path.clone(),
146 origin: crate::loader::origin_in(&figment, path),
147 })
148 .collect();
149
150 Ok(Report {
151 key: spec.key.to_owned(),
152 resolved,
153 unknown: unknown_keys(&snapshot, fields, &aliased_keys(spec)),
157 failure: crate::loader::load::<T>(spec)
160 .err()
161 .map(|error| error.to_string()),
162 })
163}
164
165fn aliased_keys(spec: &LoadSpec<'_>) -> Vec<String> {
167 spec.aliases
168 .map(crate::Aliases::known_keys)
169 .unwrap_or_default()
170}
171
172fn unknown_keys(snapshot: &Snapshot, fields: &[&str], aliased: &[String]) -> Vec<UnknownKey> {
173 if fields.is_empty() {
174 return Vec::new();
175 }
176
177 snapshot
178 .top_level_keys()
179 .into_iter()
180 .filter(|key| !fields.contains(&key.as_str()))
181 .filter(|key| !aliased.iter().any(|alias| alias == key))
182 .map(|key| UnknownKey {
183 suggestion: closest(&key, fields),
184 path: key,
185 })
186 .collect()
187}
188
189fn closest(key: &str, fields: &[&str]) -> Option<String> {
196 let budget = (key.len() / 4).max(1);
197
198 fields
199 .iter()
200 .map(|field| (distance(key, field), *field))
201 .filter(|(distance, _)| *distance <= budget)
202 .min_by_key(|(distance, _)| *distance)
203 .map(|(_, field)| field.to_owned())
204}
205
206fn distance(left: &str, right: &str) -> usize {
212 let left: Vec<char> = left.chars().collect();
213 let right: Vec<char> = right.chars().collect();
214
215 let mut before_last = vec![0; right.len() + 1];
217 let mut previous: Vec<usize> = (0..=right.len()).collect();
218 let mut current = vec![0; right.len() + 1];
219
220 for i in 0..left.len() {
221 current[0] = i + 1;
222
223 for j in 0..right.len() {
224 let substitution = usize::from(left[i] != right[j]);
225
226 current[j + 1] = (previous[j] + substitution)
227 .min(previous[j + 1] + 1)
228 .min(current[j] + 1);
229
230 if i > 0 && j > 0 && left[i] == right[j - 1] && left[i - 1] == right[j] {
232 current[j + 1] = current[j + 1].min(before_last[j - 1] + 1);
233 }
234 }
235
236 std::mem::swap(&mut before_last, &mut previous);
237 std::mem::swap(&mut previous, &mut current);
238 }
239
240 previous[right.len()]
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn a_close_miss_is_suggested() {
249 assert_eq!(closest("hsot", &["host", "port"]).as_deref(), Some("host"));
250 assert_eq!(closest("prot", &["host", "port"]).as_deref(), Some("port"));
251 assert_eq!(closest("hosts", &["host", "port"]).as_deref(), Some("host"));
252 }
253
254 #[test]
255 fn an_unrelated_key_suggests_nothing() {
256 assert_eq!(closest("elephant", &["host", "port"]), None);
257 }
258
259 #[test]
260 fn the_budget_scales_with_the_name() {
261 assert_eq!(closest("od", &["id"]).as_deref(), Some("id"));
263 assert_eq!(
265 closest("conection_timout", &["connection_timeout"]).as_deref(),
266 Some("connection_timeout")
267 );
268 assert_eq!(closest("xy", &["id"]), None);
270 }
271
272 #[test]
273 fn distance_counts_edits() {
274 assert_eq!(distance("", ""), 0);
275 assert_eq!(distance("host", "host"), 0);
276 assert_eq!(distance("host", ""), 4);
277 assert_eq!(distance("port", "sport"), 1, "one insertion");
278 assert_eq!(distance("port", "pxrt"), 1, "one substitution");
279 }
280
281 #[test]
282 fn a_transposition_is_one_edit_not_two() {
283 assert_eq!(distance("port", "prot"), 1);
285 assert_eq!(distance("host", "hsot"), 1);
286 }
287
288 #[test]
289 fn an_unknown_key_renders_its_suggestion() {
290 let unknown = UnknownKey {
291 path: "hsot".to_owned(),
292 suggestion: Some("host".to_owned()),
293 };
294
295 assert_eq!(
296 unknown.to_string(),
297 "hsot: unknown key, did you mean `host`?"
298 );
299 }
300}