Skip to main content

four_word_networking/
validation.rs

1//! Validation and autocomplete functionality for four-word networking.
2
3use crate::dictionary4k::DICTIONARY;
4use crate::error::Result;
5use serde::{Deserialize, Serialize};
6
7/// Result of validating partial input
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ValidationResult {
10    /// Whether the current prefix is valid
11    pub is_valid_prefix: bool,
12    /// Possible completions for the current word
13    pub possible_completions: Vec<String>,
14    /// Number of complete words entered so far
15    pub word_count_so_far: usize,
16    /// Expected total number of words (4 for IPv4, 6/9/12 for IPv6)
17    pub expected_total_words: Option<usize>,
18    /// Whether the input is complete
19    pub is_complete: bool,
20}
21
22impl ValidationResult {
23    /// Create a validation result for incomplete input
24    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    /// Create a validation result for complete input
35    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
46/// Autocomplete helper for four-word networking
47pub struct AutocompleteHelper;
48
49impl AutocompleteHelper {
50    /// Get word hints for a given prefix
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use four_word_networking::validation::AutocompleteHelper;
56    ///
57    /// let hints = AutocompleteHelper::get_word_hints("bea");
58    /// assert!(!hints.is_empty());
59    /// assert!(hints.iter().all(|w| w.starts_with("bea")));
60    /// ```
61    pub fn get_word_hints(prefix: &str) -> Vec<String> {
62        DICTIONARY.get_word_hints(prefix)
63    }
64
65    /// Validate partial input and provide suggestions
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use four_word_networking::validation::AutocompleteHelper;
71    ///
72    /// let result = AutocompleteHelper::validate_partial_input("beach cont").unwrap();
73    /// assert!(result.is_valid_prefix);
74    /// assert_eq!(result.word_count_so_far, 1);
75    /// assert!(!result.possible_completions.is_empty());
76    /// ```
77    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        // Split input into words
83        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        // Count complete words (those that exist in dictionary)
90        let mut complete_words = 0;
91        for (i, part) in parts.iter().enumerate() {
92            if i == parts.len() - 1 {
93                // Last part might be incomplete
94                if DICTIONARY.get_index(part).is_some() {
95                    complete_words += 1;
96                } else {
97                    // It's a partial word, get hints
98                    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                // All non-last parts must be complete words
108                if DICTIONARY.get_index(part).is_some() {
109                    complete_words += 1;
110                } else {
111                    // Invalid word in the middle
112                    return Ok(ValidationResult::incomplete(false, vec![], complete_words));
113                }
114            }
115        }
116
117        // Check if we have a valid complete address
118        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    /// Suggest completions for partial words
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use four_word_networking::validation::AutocompleteHelper;
131    ///
132    /// let suggestions = AutocompleteHelper::suggest_completions("beach cont").unwrap();
133    /// assert!(!suggestions.is_empty());
134    /// ```
135    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        // Get the base (complete words so far)
143        let parts: Vec<&str> = partial_words.split_whitespace().collect();
144
145        // Check if the last part is a partial word or if we need new suggestions
146        let (base, need_completions) = if parts.is_empty() {
147            (String::new(), true)
148        } else if !validation.possible_completions.is_empty() {
149            // Has a partial word that needs completion
150            if parts.len() > 1 {
151                (parts[..parts.len() - 1].join(" "), false)
152            } else {
153                (String::new(), false)
154            }
155        } else {
156            // All complete words, need to suggest next word
157            (partial_words.to_string(), true)
158        };
159
160        let mut suggestions = Vec::new();
161
162        if need_completions {
163            // Suggest some common starting words
164            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            // Use the validation's possible completions
180            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    /// Auto-complete if there's a unique match at 5 characters
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use four_word_networking::validation::AutocompleteHelper;
198    ///
199    /// // With 5 unique characters
200    /// let completed = AutocompleteHelper::auto_complete_at_five("beach");
201    /// assert_eq!(completed, Some("beach".to_string()));
202    ///
203    /// // With less than 5 characters
204    /// let not_completed = AutocompleteHelper::auto_complete_at_five("bea");
205    /// assert_eq!(not_completed, None);
206    /// ```
207    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    /// Suggest corrections for potentially misspelled words
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use four_word_networking::validation::AutocompleteHelper;
221    ///
222    /// // This is a simple example - real implementation would use
223    /// // edit distance algorithms for better suggestions
224    /// let corrections = AutocompleteHelper::suggest_corrections("beech");
225    /// assert!(!corrections.is_empty());
226    /// ```
227    pub fn suggest_corrections(word: &str) -> Vec<String> {
228        // First check if it's a valid word
229        if DICTIONARY.get_index(word).is_some() {
230            return vec![word.to_string()];
231        }
232
233        // Try to find similar words by prefix
234        let mut suggestions = Vec::new();
235
236        // Check progressively shorter prefixes
237        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        // We need to use actual words from the dictionary
274        // Let's assume "about" is in the dictionary
275        if DICTIONARY.get_index("about").is_some() {
276            // Create a valid 4-word combination (if these words exist)
277            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        // Test with 5+ character prefix
293        let word = AutocompleteHelper::auto_complete_at_five("about");
294        // Should return Some if "about" exists and is unique at 5 chars
295        assert!(word.is_some() || word.is_none());
296
297        // Test with less than 5 characters
298        let word = AutocompleteHelper::auto_complete_at_five("abo");
299        assert_eq!(word, None);
300    }
301
302    #[test]
303    fn test_suggest_completions() {
304        // First check if we have words starting with "a"
305        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); // Limited to 10 suggestions
311    }
312
313    #[test]
314    fn test_suggest_corrections() {
315        // Test with a valid word
316        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        // Test with an invalid word
322        let corrections = AutocompleteHelper::suggest_corrections("aboot");
323        assert!(!corrections.is_empty()); // Should suggest something
324    }
325}