#![allow(dead_code)]
use proc_macro2::Span;
pub fn suggest(key: &str, candidates: &[String]) -> Option<String> {
candidates
.iter()
.map(|c| (c, strsim::jaro_winkler(key, c)))
.filter(|(_, s)| *s >= 0.85)
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(c, _)| c.clone())
}
pub fn unknown_top_key_error(
key: &str,
key_span: Span,
allowed: &[&'static str],
op_name: &str,
) -> syn::Error {
let owned: Vec<String> = allowed.iter().map(|s| (*s).to_string()).collect();
let suggestion = suggest(key, &owned);
let msg = match suggestion {
Some(c) => format!(
"unknown top-level key `{key}` for `{op_name}!`. did you mean `{c}`? \
Allowed keys: {allowed:?}"
),
None => {
format!("unknown top-level key `{key}` for `{op_name}!`. Allowed keys: {allowed:?}")
}
};
syn::Error::new(key_span, msg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suggest_returns_typo_neighbor() {
let candidates = vec!["email".to_string(), "name".to_string(), "id".to_string()];
assert_eq!(suggest("emial", &candidates).as_deref(), Some("email"));
}
#[test]
fn suggest_returns_none_when_too_far() {
let candidates = vec!["email".to_string()];
assert!(suggest("xyz", &candidates).is_none());
}
#[test]
fn suggest_picks_highest_when_multiple_close() {
let candidates = vec!["email".to_string(), "emails".to_string()];
assert_eq!(suggest("emial", &candidates).as_deref(), Some("email"));
}
#[test]
fn unknown_top_key_error_includes_suggestion_when_close() {
let err = unknown_top_key_error(
"wher",
Span::call_site(),
&["where", "include", "select"],
"find_many",
);
let msg = err.to_string();
assert!(msg.contains("did you mean"), "got: {msg}");
assert!(msg.contains("where"), "got: {msg}");
}
#[test]
fn unknown_top_key_error_omits_suggestion_when_far() {
let err = unknown_top_key_error(
"xyzzy",
Span::call_site(),
&["where", "include", "select"],
"find_many",
);
let msg = err.to_string();
assert!(!msg.contains("did you mean"), "got: {msg}");
}
}