1#![warn(missing_docs)]
20
21use std::fmt;
22use std::collections::HashSet;
23use rand::{rngs::OsRng, seq::SliceRandom};
24
25pub const WORDS: &'static [&'static str] = &include!("../res/wordlist.json");
28
29pub fn random_phrase(no_of_words: usize) -> String {
35 let mut rng = OsRng;
36 (0..no_of_words).map(|_| WORDS.choose(&mut rng).unwrap()).fold(String::new(), |mut acc, word| {
37 acc.push_str(" ");
38 acc.push_str(word);
39 acc
40 }).trim_start().to_owned()
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub enum Error {
46 PhraseTooShort(usize),
48 WordNotFromDictionary(String),
50}
51
52impl fmt::Display for Error {
53 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
54 match *self {
55 Error::PhraseTooShort(len) => writeln!(fmt, "The phrase is too short ({})", len),
56 Error::WordNotFromDictionary(ref word) => writeln!(fmt, "The word '{}' does not come from the dictionary.", word),
57 }
58 }
59}
60
61pub fn validate_phrase(phrase: &str, expected_no_of_words: usize) -> Result<(), Error> {
65 lazy_static::lazy_static! {
66 static ref WORD_SET: HashSet<&'static str> = WORDS.iter().cloned().collect();
67 }
68
69 let mut len = 0;
70 for word in phrase.split_whitespace() {
71 len += 1;
72 if !WORD_SET.contains(word) {
73 return Err(Error::WordNotFromDictionary(word.into()));
74 }
75 }
76
77 if len < expected_no_of_words {
78 return Err(Error::PhraseTooShort(len));
79 }
80
81 return Ok(());
82}
83
84#[cfg(test)]
85mod tests {
86 use super::{validate_phrase, random_phrase, Error};
87
88 #[test]
89 fn should_produce_right_number_of_words() {
90 let p = random_phrase(10);
91 assert_eq!(p.split(" ").count(), 10);
92 }
93
94 #[test]
95 fn should_not_include_carriage_return() {
96 let p = random_phrase(10);
97 assert!(!p.contains('\r'), "Carriage return should be trimmed.");
98 }
99
100 #[test]
101 fn should_validate_the_phrase() {
102 let p = random_phrase(10);
103
104 assert_eq!(validate_phrase(&p, 10), Ok(()));
105 assert_eq!(validate_phrase(&p, 12), Err(Error::PhraseTooShort(10)));
106 assert_eq!(validate_phrase("xxx", 0), Err(Error::WordNotFromDictionary("xxx".into())));
107 }
108}