four_word_networking/
validation.rs1use crate::dictionary4k::DICTIONARY;
4use crate::error::Result;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ValidationResult {
10 pub is_valid_prefix: bool,
12 pub possible_completions: Vec<String>,
14 pub word_count_so_far: usize,
16 pub expected_total_words: Option<usize>,
18 pub is_complete: bool,
20}
21
22impl ValidationResult {
23 pub fn incomplete(is_valid: bool, completions: Vec<String>, word_count: usize) -> Self {
25 ValidationResult {
26 is_valid_prefix: is_valid,
27 possible_completions: completions,
28 word_count_so_far: word_count,
29 expected_total_words: None,
30 is_complete: false,
31 }
32 }
33
34 pub fn complete(word_count: usize) -> Self {
36 ValidationResult {
37 is_valid_prefix: true,
38 possible_completions: vec![],
39 word_count_so_far: word_count,
40 expected_total_words: Some(word_count),
41 is_complete: true,
42 }
43 }
44}
45
46pub struct AutocompleteHelper;
48
49impl AutocompleteHelper {
50 pub fn get_word_hints(prefix: &str) -> Vec<String> {
62 DICTIONARY.get_word_hints(prefix)
63 }
64
65 pub fn validate_partial_input(partial: &str) -> Result<ValidationResult> {
78 if partial.is_empty() {
79 return Ok(ValidationResult::incomplete(true, vec![], 0));
80 }
81
82 let parts: Vec<&str> = partial.split_whitespace().collect();
84
85 if parts.is_empty() {
86 return Ok(ValidationResult::incomplete(true, vec![], 0));
87 }
88
89 let mut complete_words = 0;
91 for (i, part) in parts.iter().enumerate() {
92 if i == parts.len() - 1 {
93 if DICTIONARY.get_index(part).is_some() {
95 complete_words += 1;
96 } else {
97 let hints = DICTIONARY.get_word_hints(part);
99 let is_valid = !hints.is_empty();
100 return Ok(ValidationResult::incomplete(
101 is_valid,
102 hints,
103 complete_words,
104 ));
105 }
106 } else {
107 if DICTIONARY.get_index(part).is_some() {
109 complete_words += 1;
110 } else {
111 return Ok(ValidationResult::incomplete(false, vec![], complete_words));
113 }
114 }
115 }
116
117 match complete_words {
119 4 => Ok(ValidationResult::complete(4)),
120 6 | 9 | 12 => Ok(ValidationResult::complete(complete_words)),
121 _ => Ok(ValidationResult::incomplete(true, vec![], complete_words)),
122 }
123 }
124
125 pub fn suggest_completions(partial_words: &str) -> Result<Vec<String>> {
136 let validation = Self::validate_partial_input(partial_words)?;
137
138 if validation.is_complete {
139 return Ok(vec![partial_words.to_string()]);
140 }
141
142 let parts: Vec<&str> = partial_words.split_whitespace().collect();
144
145 let (base, need_completions) = if parts.is_empty() {
147 (String::new(), true)
148 } else if !validation.possible_completions.is_empty() {
149 if parts.len() > 1 {
151 (parts[..parts.len() - 1].join(" "), false)
152 } else {
153 (String::new(), false)
154 }
155 } else {
156 (partial_words.to_string(), true)
158 };
159
160 let mut suggestions = Vec::new();
161
162 if need_completions {
163 let common_prefixes = vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
165 for prefix in common_prefixes {
166 let hints = DICTIONARY.get_word_hints(prefix);
167 if let Some(first) = hints.first() {
168 if base.is_empty() {
169 suggestions.push(first.clone());
170 } else {
171 suggestions.push(format!("{base} {first}"));
172 }
173 if suggestions.len() >= 10 {
174 break;
175 }
176 }
177 }
178 } else {
179 for completion in validation.possible_completions.iter().take(10) {
181 if base.is_empty() {
182 suggestions.push(completion.clone());
183 } else {
184 suggestions.push(format!("{base} {completion}"));
185 }
186 }
187 }
188
189 Ok(suggestions)
190 }
191
192 pub fn auto_complete_at_five(prefix: &str) -> Option<String> {
208 if prefix.len() >= 5 {
209 DICTIONARY.get_unique_word_for_prefix(prefix)
210 } else {
211 None
212 }
213 }
214
215 pub fn suggest_corrections(word: &str) -> Vec<String> {
228 if DICTIONARY.get_index(word).is_some() {
230 return vec![word.to_string()];
231 }
232
233 let mut suggestions = Vec::new();
235
236 for len in (1..=word.len().min(5)).rev() {
238 let prefix = &word[..len];
239 let hints = DICTIONARY.get_word_hints(prefix);
240 if !hints.is_empty() {
241 suggestions.extend(hints.into_iter().take(5));
242 break;
243 }
244 }
245
246 suggestions
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_validation_empty() {
256 let result = AutocompleteHelper::validate_partial_input("").unwrap();
257 assert!(result.is_valid_prefix);
258 assert_eq!(result.word_count_so_far, 0);
259 assert!(!result.is_complete);
260 }
261
262 #[test]
263 fn test_validation_partial_word() {
264 let result = AutocompleteHelper::validate_partial_input("abo").unwrap();
265 assert!(result.is_valid_prefix);
266 assert_eq!(result.word_count_so_far, 0);
267 assert!(!result.possible_completions.is_empty());
268 assert!(!result.is_complete);
269 }
270
271 #[test]
272 fn test_validation_complete_ipv4() {
273 if DICTIONARY.get_index("about").is_some() {
276 let test_words = ["about", "above", "absent", "accept"];
278 let all_valid = test_words.iter().all(|w| DICTIONARY.get_index(w).is_some());
279
280 if all_valid {
281 let input = test_words.join(" ");
282 let result = AutocompleteHelper::validate_partial_input(&input).unwrap();
283 assert!(result.is_valid_prefix);
284 assert_eq!(result.word_count_so_far, 4);
285 assert!(result.is_complete);
286 }
287 }
288 }
289
290 #[test]
291 fn test_auto_complete_at_five() {
292 let word = AutocompleteHelper::auto_complete_at_five("about");
294 assert!(word.is_some() || word.is_none());
296
297 let word = AutocompleteHelper::auto_complete_at_five("abo");
299 assert_eq!(word, None);
300 }
301
302 #[test]
303 fn test_suggest_completions() {
304 let hints = AutocompleteHelper::get_word_hints("a");
306 assert!(!hints.is_empty(), "Should have words starting with 'a'");
307
308 let suggestions = AutocompleteHelper::suggest_completions("a").unwrap();
309 assert!(!suggestions.is_empty(), "Should have suggestions for 'a'");
310 assert!(suggestions.len() <= 10); }
312
313 #[test]
314 fn test_suggest_corrections() {
315 if DICTIONARY.get_index("about").is_some() {
317 let corrections = AutocompleteHelper::suggest_corrections("about");
318 assert_eq!(corrections, vec!["about".to_string()]);
319 }
320
321 let corrections = AutocompleteHelper::suggest_corrections("aboot");
323 assert!(!corrections.is_empty()); }
325}