search_hub

at 861f6ab Raw

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
}