use serde::Deserialize;
use super::SourceAdapter;
use crate::model::{Match, Query, Source};
use crate::Result;
const DEFAULT_BASE_URL: &str = "https://crates.io";
const USER_AGENT: &str = concat!(
"patent/",
env!("CARGO_PKG_VERSION"),
" (prior-art search; https://github.com/riad/patent)"
);
#[derive(Debug, Clone)]
pub struct CratesIo {
client: reqwest::Client,
base_url: String,
}
impl CratesIo {
pub fn new(client: reqwest::Client) -> Self {
Self::with_base_url(client, DEFAULT_BASE_URL.to_string())
}
pub fn with_base_url(client: reqwest::Client, base_url: String) -> Self {
Self { client, base_url }
}
}
#[derive(Debug, Deserialize)]
struct SearchResponse {
crates: Vec<CrateHit>,
}
#[derive(Debug, Deserialize)]
struct CrateHit {
name: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
downloads: Option<u64>,
}
#[async_trait::async_trait]
impl SourceAdapter for CratesIo {
fn id(&self) -> Source {
Source::CratesIo
}
async fn search(&self, query: &Query) -> Result<Vec<Match>> {
let url = format!("{}/api/v1/crates", self.base_url);
let q = query.keywords.join(" ");
let response = self
.client
.get(&url)
.header(reqwest::header::USER_AGENT, USER_AGENT)
.query(&[("q", q.as_str()), ("per_page", "20")])
.send()
.await?
.error_for_status()?;
let body: SearchResponse = response.json().await?;
Ok(body
.crates
.into_iter()
.map(|c| Match {
url: format!("{DEFAULT_BASE_URL}/crates/{}", c.name),
name: c.name,
source: Source::CratesIo,
description: c.description.unwrap_or_default(),
popularity: c.downloads,
similarity: 0.0,
})
.collect())
}
}