use async_trait::async_trait; use serde::Deserialize; use crate::search_engines::{EngineError, ResultEntry, SearchEngine}; use crate::search_engines::utils::urlencode; pub const DEFAULT_LOCALE: &str = "en-US"; pub struct Mdn { pub locale: String, pub timeout_secs: Option<f32>, } #[derive(Deserialize)] struct MdnResult { title: String, summary: Option<String>, mdn_url: String, } #[derive(Deserialize)] struct MdnResponse { documents: Vec<MdnResult>, } #[async_trait] impl SearchEngine for Mdn { fn id(&self) -> &str { "mdn" } fn name(&self) -> &str { "MDN" } fn url_template(&self) -> &str { "https://developer.mozilla.org/{locale}/docs/..." } fn selector(&self) -> &str { "" } fn timeout(&self) -> std::time::Duration { self.timeout_secs .map(std::time::Duration::from_secs_f32) .unwrap_or_else(|| std::time::Duration::from_secs(5)) } async fn fetch_results( &self, query: &str, client: &reqwest::Client, ) -> Result<Vec<ResultEntry>, EngineError> { let url = format!( "https://developer.mozilla.org/api/v1/search?q={}&locale={}", urlencode(query), self.locale ); let body = client .get(&url) .send() .await .map_err(|e| EngineError(format!("fetch failed: {e}")))? .text() .await .map_err(|e| EngineError(format!("read body failed: {e}")))?; let resp: MdnResponse = serde_json::from_str(&body) .map_err(|e| EngineError(format!("JSON parse failed: {e}")))?; let results: Vec<ResultEntry> = resp .documents .into_iter() .map(|d| { let page_url = format!("https://developer.mozilla.org{}", d.mdn_url); ResultEntry { title: strip_mark_tags(&d.title), url: page_url, description: d.summary.map(|s| strip_mark_tags(&s)), engine: format!("mdn.{}", self.locale), } }) .collect(); if results.is_empty() { Err(EngineError("no results found".into())) } else { Ok(results) } } } fn strip_mark_tags(s: &str) -> String { let s = s.replace("<mark>", ""); s.replace("</mark>", "") } #[cfg(test)] mod tests { use super::*; use crate::search_engines::SearchEngine; #[test] fn test_id() { let e = Mdn { locale: DEFAULT_LOCALE.into(), timeout_secs: None }; assert_eq!(e.id(), "mdn"); } #[test] fn test_name() { let e = Mdn { locale: DEFAULT_LOCALE.into(), timeout_secs: None }; assert_eq!(e.name(), "MDN"); } #[test] fn test_selector() { let e = Mdn { locale: DEFAULT_LOCALE.into(), timeout_secs: None }; assert_eq!(e.selector(), ""); } #[test] fn test_engine_construct() { let e = Mdn { locale: "fr".into(), timeout_secs: Some(3.0) }; assert_eq!(e.locale, "fr"); assert_eq!(e.timeout_secs, Some(3.0)); } #[test] fn test_strip_mark_tags() { assert_eq!(strip_mark_tags("hello"), "hello"); assert_eq!(strip_mark_tags("<mark>hello</mark>"), "hello"); assert_eq!(strip_mark_tags("hello <mark>world</mark>"), "hello world"); } }