apollo_federation/error/
suggestion.rs

1use itertools::Itertools;
2use levenshtein::levenshtein;
3
4use crate::utils::human_readable;
5
6pub(crate) fn suggestion_list(
7    input: &str,
8    options: impl IntoIterator<Item = String>,
9) -> Vec<String> {
10    let threshold = 1 + (input.len() as f64 * 0.4).floor() as usize;
11    let input_lowercase = input.to_lowercase();
12    let mut result = Vec::new();
13    for option in options {
14        // Special casing so that if the only mismatch is in upper/lower-case, then the option is
15        // always shown.
16        let distance = if input_lowercase == option.to_lowercase() {
17            1
18        } else {
19            levenshtein(input, &option)
20        };
21        if distance <= threshold {
22            result.push((option, distance));
23        }
24    }
25    result.sort_by(|x, y| x.1.cmp(&y.1));
26    result.into_iter().map(|(s, _)| s.to_string()).collect()
27}
28
29const MAX_SUGGESTIONS: usize = 5;
30
31/// Given [ A, B ], returns "Did you mean A or B?".
32/// Given [ A, B, C ], returns "Did you mean A, B, or C?".
33pub(crate) fn did_you_mean(suggestions: impl IntoIterator<Item = String>) -> String {
34    const MESSAGE: &str = "Did you mean ";
35    let suggestions = suggestions
36        .into_iter()
37        .take(MAX_SUGGESTIONS)
38        .map(|s| format!("\"{s}\""))
39        .collect_vec();
40    let last_separator = if suggestions.len() > 2 {
41        Some(", or ")
42    } else {
43        Some(" or ")
44    };
45    let suggestion_str = human_readable::join_strings(
46        suggestions.iter(),
47        human_readable::JoinStringsOptions {
48            separator: ", ",
49            first_separator: None,
50            last_separator,
51            output_length_limit: None,
52        },
53    );
54    format!("{MESSAGE}{suggestion_str}?")
55}