use serde::Deserialize;
use super::SourceAdapter;
use crate::freshness;
use crate::model::{Match, Query, Source};
use crate::Result;
const DEFAULT_BASE_URL: &str = "https://hex.pm";
#[derive(Debug, Clone)]
pub struct Hex {
client: reqwest::Client,
base_url: String,
}
impl Hex {
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 Package {
name: String,
#[serde(default)]
html_url: Option<String>,
#[serde(default)]
meta: Meta,
#[serde(default)]
downloads: Downloads,
#[serde(default, deserialize_with = "crate::freshness::lenient")]
updated_at: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct Meta {
#[serde(default)]
description: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct Downloads {
#[serde(default)]
all: Option<u64>,
}
#[async_trait::async_trait]
impl SourceAdapter for Hex {
fn id(&self) -> Source {
Source::Hex
}
async fn search(&self, query: &Query) -> Result<Vec<Match>> {
let url = format!("{}/api/packages", self.base_url);
let q = query.keywords.join(" ");
let body: Vec<Package> = self
.client
.get(&url)
.query(&[("search", q.as_str()), ("page", "1")])
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(body
.into_iter()
.map(|p| {
let url = p
.html_url
.filter(|u| !u.trim().is_empty())
.unwrap_or_else(|| format!("{}/packages/{}", self.base_url, p.name));
Match {
name: p.name,
source: Source::Hex,
url,
description: p.meta.description.unwrap_or_default(),
popularity: p.downloads.all.filter(|&d| d > 0),
similarity: 0.0,
last_updated: p.updated_at.as_deref().and_then(freshness::from_rfc3339),
}
})
.collect())
}
}