mewe/
lib.rs

1use reqwest::get;
2use scraper::{Html, Selector};
3use std::{error::Error, fmt};
4
5type Result<T> = std::result::Result<T, Box<dyn Error>>;
6
7#[derive(Debug, Clone)]
8struct SearchFailed;
9
10impl Error for SearchFailed {}
11
12impl fmt::Display for SearchFailed {
13    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
14        write!(
15            f,
16            "Sorry, the word you’re looking for can’t be found in the dictionary."
17        )
18    }
19}
20
21/// The result of a successful search. The search may return suggestions for words not present in the Merriam Webster dictionary.
22#[derive(Debug, PartialEq)]
23pub enum Search {
24    /// Definition returned on a successful search for an existing word in the Merriam Webster dictionary.
25    Definition(String),
26    /// Word suggestions from Merriam Webster dictionary on for a misspelled word.
27    Suggestions(String),
28}
29
30// TODO: Add doc examples for using search()
31/// Takes the word query as input("String").
32/// Returns a definition if the word was found in the Merriam Webster dictionary.
33/// Returns suggestions if the word was misspelled.
34/// Returns an error if the word was utterly misspelled.
35pub async fn search(query: String) -> Result<Search> {
36    let html = get(format!(
37        "https://www.merriam-webster.com/dictionary/{}",
38        query
39    ))
40    .await?
41    .text()
42    .await?;
43    let document = Html::parse_document(&html);
44    if let Ok(mut definition) = definition(document.clone()).await {
45        // Definition ends with unnecessary text: "How to use..."
46        // Remove that by finding the last match of the pattern.
47        let index = definition.rfind("How to use").unwrap_or(definition.len());
48        definition.truncate(index);
49        return Ok(Search::Definition(definition.trim().to_string()));
50    }
51    if let Ok(suggestions) = suggestions(document).await {
52        if !suggestions.is_empty() {
53            return Ok(Search::Suggestions(suggestions.trim().to_string()));
54        }
55    }
56    Err(Box::new(SearchFailed))
57}
58
59/// Searches for the "meta" tag with the attribute: name="description"
60/// Extracts the text inside the "content" attribute in the same meta tag
61/// If the tag is not found then returns an error else returns the definition
62async fn definition(document: Html) -> Result<String> {
63    // Selector for definition: <meta name="description" content="The meaning of STUFF is materials, supplies, or equipment used in various activities. How to use stuff in a sentence.">
64    let def_sel = Selector::parse(r#"meta[name="description"]"#)?;
65    match document
66        .select(&def_sel)
67        .next()
68        .and_then(|node| node.value().attr("content"))
69    {
70        Some(definition) => Ok(definition.into()),
71        _ => Err(Box::new(SearchFailed)),
72    }
73}
74
75/// Searches for the "p" tag with class name "spelling-suggestions"
76/// Extracts the text inside the "a" tags inside the "p" tags
77/// If the tag is not found or the text is empty then returns an error
78async fn suggestions(document: Html) -> Result<String> {
79    // Selector for: <p class="spelling-suggestions"><a href="/medical/sluff">sluff</a></p>
80    let mut suggestions = String::new();
81    let ss_sel = Selector::parse("p.spelling-suggestions")?;
82    for node in document.select(&ss_sel) {
83        suggestions.push_str(&node.text().collect::<String>());
84        suggestions.push(',');
85    }
86    Ok(suggestions)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::{search, Search};
92    #[tokio::test]
93    async fn query_dictionary() {
94        // Searching an existing word "dictionary"
95        let definition = search("dictionary".to_string()).await;
96        let saved_definition = Search::Definition("The meaning of DICTIONARY is a reference source in print or electronic form containing words usually alphabetically arranged along with information about their forms, pronunciations, functions, etymologies, meanings, and syntactic and idiomatic uses.".to_string());
97        assert!(definition.is_ok_and(|definition| definition == saved_definition));
98    }
99    #[tokio::test]
100    async fn misspelling() {
101        // Searching a misspelled word "dictionar"
102        let suggestions = search("dictionar".to_string()).await;
103        let saved_suggestions = Search::Suggestions("dictionary,dictional,diction,dictions,dictionaries,fictional,dictionally,factionary,diactinal,frictional,dictyonine,lectionary,diactin,sectionary,diactine,duction,indicational,miction,discretionary,fiction,".to_string());
104        assert!(suggestions.is_ok_and(|suggestions| suggestions == saved_suggestions));
105    }
106    #[tokio::test]
107    async fn search_garbage_word() {
108        // Searching a garbage word "zqrxg" with possibly no suggestions
109        assert!(search("zqrxg".to_string()).await.is_err());
110    }
111}