1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]

//! # random_word
//!
//! The `random_word` crate provides a simple way to generate random 
//! english words.
//!
//! The [`random_word`][Words] associated functions are asynchronous
//! calls to a third party api.
//! 
//! ## Generating a random word
//!
//! ```rust
//! use random_word::Words;
//! # async fn random_word() -> Result<(), random_word::Error> {
//! let word = Words::random().await;
//! # Ok(())
//! # }
//! ```

static SINGLE_WORD_URL: &str = "https://random-word-api.herokuapp.com/word";
static ALL_WORD_URL: &str = "https://random-word-api.herokuapp.com/all";


/// A `Result` alias where the `Err` case is `random_word::Error`.
pub type Result<T> = std::result::Result<T, Error>;

/// The Errors that may occur.
///
/// Note: This is an enumumation of dependency crate errors
#[derive(Debug)]
pub enum Error {
    /// All networking errors which can occur.
    Network(reqwest::Error),
    /// All json parsing errors which can occur.
    ParseJson(serde_json::Error)
}


/// An empty struct for calling associated functions.
#[derive(Debug)]
pub struct Words;

impl Words {
    /// Generates a random english word.
    /// 
    /// **NOTE**: This function makes a http request.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use random_word::Words;
    /// # async fn random_word() -> Result<(), random_word::Error> {
    /// let word = Words::random().await;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// This function fails if:
    ///
    /// - there is an error fetching data from the third party provider
    pub async fn random() -> Result<String> {
        fetch(SINGLE_WORD_URL).await
    }

    /// Generates all known english words sorted alphabetically.
    /// 
    /// **NOTE**: This function makes a http request.
    ///
    /// **WARNING**: This is a large dataset (>2MB).
    /// 
    /// # Examples
    ///
    /// ```rust
    /// use random_word::Words;
    /// # async fn all_words() -> Result<(), random_word::Error> {
    /// let all_words = Words::all().await;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// This function fails if:
    ///
    /// - there is an error fetching data from the third party provider
    /// - the data received from the third party provider is malformed
    pub async fn all() -> Result<Vec<String>> {
        fetch(ALL_WORD_URL).await
            .and_then(|s| from_json::<Vec<String>>(&s))
    }
}

async fn fetch(url: &str) -> Result<String> {
    match reqwest::get(url).await {
        Ok(response) => match response.text().await {
            Ok(data) => Ok(data),
            Err(error) => Err(Error::Network(error))
        },
        Err(error) => Err(Error::Network(error))
    }
}

fn from_json<T>(text: &str) -> Result<T>
    where T: serde::de::DeserializeOwned {
    match serde_json::from_str::<T>(text) {
        Ok(data_struct) => Ok(data_struct),
        Err(error) => Err(Error::ParseJson(error))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test(flavor = "multi_thread")]
    async fn random_word() {
        let future1 = Words::random();
        let future2 = Words::random();
        match [future1.await, future2.await] {
            [Ok(word1), Ok(word2)] => {
                assert_ne!(word1,word2);
                assert!([word1,word2].iter().all(|word| {
                   !word.is_empty() &&
                    word.chars().all(|c| !('0'..='9').contains(&c))
                }))
            },
            [Err(error),_] | [_,Err(error)]  => panic!("{:#?}", error),
        }
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn all_words() {
        match Words::all().await {
            Ok(all_words) => {
                assert!(!all_words.is_empty());
                assert!(all_words.iter().all(|w| w.chars()
                    .all(|c| !('0'..='9')
                        .contains(&c) && ' ' != c)));
            },
            Err(error) => panic!("{:#?}", error)
        }
    }
}