use async_trait::async_trait; use serde::Deserialize; use crate::search_engines::{EngineError, ResultEntry, SearchEngine}; pub struct CratesIo; #[derive(Deserialize)] struct CrateResult { name: String, description: Option<String>, homepage: Option<String>, documentation: Option<String>, repository: Option<String>, } #[derive(Deserialize)] struct ApiResponse { crates: Vec<CrateResult>, } #[async_trait] impl SearchEngine for CratesIo { fn id(&self) -> &str { "crates.io" } fn name(&self) -> &str { "crates.io" } fn url_template(&self) -> &str { "https://crates.io/api/v1/crates?q={}&per_page=10" } fn selector(&self) -> &str { "" } async fn fetch_results( &self, query: &str, client: &reqwest::Client, ) -> Result<Vec<ResultEntry>, EngineError> { let url = self.search_url(query); let body = client .get(&url) .header("Accept", "application/json") .send() .await .map_err(|e| EngineError(format!("fetch failed: {e}")))? .text() .await .map_err(|e| EngineError(format!("read body failed: {e}")))?; let resp: ApiResponse = serde_json::from_str(&body) .map_err(|e| EngineError(format!("JSON parse failed: {e}")))?; let results: Vec<ResultEntry> = resp .crates .into_iter() .map(|c| { let title = format!("{}{}", c.name, c.description.as_ref().map(|d| format!(" - {d}")).unwrap_or_default(), ); let url = c .homepage .or(c.documentation) .or(c.repository) .unwrap_or_else(|| format!("https://crates.io/crates/{}", c.name)); ResultEntry { title, url, description: c.description, engine: "crates.io".into(), } }) .collect(); if results.is_empty() { Err(EngineError("no results found".into())) } else { Ok(results) } } } pub fn engine() -> CratesIo { CratesIo } #[cfg(test)] mod tests { use super::*; use crate::search_engines::SearchEngine; #[test] fn test_id() { assert_eq!(CratesIo.id(), "crates.io"); } #[test] fn test_name() { assert_eq!(CratesIo.name(), "crates.io"); } #[test] fn test_url_template() { assert_eq!( CratesIo.url_template(), "https://crates.io/api/v1/crates?q={}&per_page=10" ); } #[test] fn test_selector() { assert_eq!(CratesIo.selector(), ""); } #[test] fn test_engine_construct() { let e = engine(); assert_eq!(e.id(), "crates.io"); } }