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#[derive(Debug, PartialEq)]
23pub enum Search {
24 Definition(String),
26 Suggestions(String),
28}
29
30pub 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 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
59async fn definition(document: Html) -> Result<String> {
63 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
75async fn suggestions(document: Html) -> Result<String> {
79 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 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 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 assert!(search("zqrxg".to_string()).await.is_err());
110 }
111}