raz_validation/
suggestion.rs

1//! Option suggestion system using fuzzy matching
2
3use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
4
5/// Provides suggestions for misspelled options
6pub struct OptionSuggester {
7    matcher: SkimMatcherV2,
8}
9
10impl Default for OptionSuggester {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl OptionSuggester {
17    /// Create a new suggester
18    pub fn new() -> Self {
19        Self {
20            matcher: SkimMatcherV2::default(),
21        }
22    }
23
24    /// Get suggestions for a misspelled option
25    pub fn suggest_options(&self, input: &str, valid_options: &[&str]) -> Vec<String> {
26        let mut scored_options: Vec<(i64, String)> = valid_options
27            .iter()
28            .filter_map(|option| {
29                self.matcher
30                    .fuzzy_match(option, input)
31                    .map(|score| (score, option.to_string()))
32            })
33            .collect();
34
35        // Sort by score (higher is better)
36        scored_options.sort_by(|a, b| b.0.cmp(&a.0));
37
38        // Return top 3 suggestions
39        scored_options
40            .into_iter()
41            .take(3)
42            .map(|(_, option)| option)
43            .collect()
44    }
45
46    /// Get suggestions with a minimum score threshold
47    pub fn suggest_options_with_threshold(
48        &self,
49        input: &str,
50        valid_options: &[&str],
51        min_score: i64,
52    ) -> Vec<String> {
53        let suggestions = self.suggest_options(input, valid_options);
54
55        // Filter by minimum score
56        suggestions
57            .into_iter()
58            .filter(|option| {
59                self.matcher
60                    .fuzzy_match(option, input)
61                    .is_some_and(|score| score >= min_score)
62            })
63            .collect()
64    }
65
66    /// Check if an option is similar enough to suggest
67    pub fn is_similar(&self, input: &str, option: &str, threshold: i64) -> bool {
68        self.matcher
69            .fuzzy_match(option, input)
70            .is_some_and(|score| score >= threshold)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_basic_suggestions() {
80        let suggester = OptionSuggester::new();
81        let options = vec!["--release", "--debug", "--verbose", "--quiet"];
82
83        let suggestions = suggester.suggest_options("--relase", &options);
84        assert!(suggestions.contains(&"--release".to_string()));
85    }
86
87    #[test]
88    fn test_no_suggestions_for_very_different_input() {
89        let suggester = OptionSuggester::new();
90        let options = vec!["--release", "--debug", "--verbose"];
91
92        let suggestions = suggester.suggest_options_with_threshold("--xyz123", &options, 20);
93        assert!(suggestions.is_empty());
94    }
95
96    #[test]
97    fn test_similarity_check() {
98        let suggester = OptionSuggester::new();
99
100        assert!(suggester.is_similar("--relase", "--release", 30));
101        assert!(!suggester.is_similar("--xyz", "--release", 30));
102    }
103}