use reqwest::get;
use scraper::{Html, Selector};
use std::{error::Error, fmt};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Debug, Clone)]
struct SearchFailed;
impl Error for SearchFailed {}
impl fmt::Display for SearchFailed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Sorry, the word you’re looking for can’t be found in the dictionary."
)
}
}
#[derive(Debug, PartialEq)]
pub enum Search {
Definition(String),
Suggestions(String),
}
pub async fn search(query: String) -> Result<Search> {
let html = get(format!(
"https://www.merriam-webster.com/dictionary/{}",
query
))
.await?
.text()
.await?;
let document = Html::parse_document(&html);
if let Ok(mut definition) = definition(document.clone()).await {
let index = definition.rfind("How to use").unwrap_or(definition.len());
definition.truncate(index);
return Ok(Search::Definition(definition.trim().to_string()));
}
if let Ok(suggestions) = suggestions(document).await {
if !suggestions.is_empty() {
return Ok(Search::Suggestions(suggestions.trim().to_string()));
}
}
Err(Box::new(SearchFailed))
}
async fn definition(document: Html) -> Result<String> {
let def_sel = Selector::parse(r#"meta[name="description"]"#)?;
match document
.select(&def_sel)
.next()
.and_then(|node| node.value().attr("content"))
{
Some(definition) => Ok(definition.into()),
_ => Err(Box::new(SearchFailed)),
}
}
async fn suggestions(document: Html) -> Result<String> {
let mut suggestions = String::new();
let ss_sel = Selector::parse("p.spelling-suggestions")?;
for node in document.select(&ss_sel) {
suggestions.push_str(&node.text().collect::<String>());
suggestions.push(',');
}
Ok(suggestions)
}
#[cfg(test)]
mod tests {
use super::{search, Search};
#[tokio::test]
async fn query_dictionary() {
let definition = search("dictionary".to_string()).await;
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());
assert!(definition.is_ok_and(|definition| definition == saved_definition));
}
#[tokio::test]
async fn misspelling() {
let suggestions = search("dictionar".to_string()).await;
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());
assert!(suggestions.is_ok_and(|suggestions| suggestions == saved_suggestions));
}
#[tokio::test]
async fn search_garbage_word() {
assert!(search("zqrxg".to_string()).await.is_err());
}
}