use anyhow::{Result, Context};
pub fn fetch_latest_version(group_id: &str, artifact_id: &str) -> Result<String> {
let query = format!("g:{} AND a:{}",
urlencoding::encode(group_id),
urlencoding::encode(artifact_id)
);
let url = format!(
"https://search.maven.org/solrsearch/select?q={}&rows=1&wt=json&sort=version desc",
urlencoding::encode(&query)
);
let response = ureq::get(&url)
.call()
.context("Failed to query Maven Central")?;
let body = response.into_string()
.context("Failed to read response from Maven Central")?;
let json: serde_json::Value = serde_json::from_str(&body)
.context("Failed to parse Maven Central response")?;
let docs = json["response"]["docs"]
.as_array()
.context("Invalid response format from Maven Central")?;
if docs.is_empty() {
return Err(anyhow::anyhow!(
"No versions found for {group_id}:{artifact_id} on Maven Central"
));
}
let doc = &docs[0];
let version = doc["latestVersion"]
.as_str()
.or_else(|| doc["v"].as_str())
.context("No version found in Maven Central response")?;
Ok(version.to_string())
}
pub fn search_maven_central(query: &str, limit: usize) -> Result<Vec<MavenCentralResult>> {
let url = format!(
"https://search.maven.org/solrsearch/select?q={}&rows={}&wt=json",
urlencoding::encode(query),
limit
);
let response = ureq::get(&url)
.call()
.context("Failed to search Maven Central")?;
let body = response.into_string()
.context("Failed to read response from Maven Central")?;
let json: serde_json::Value = serde_json::from_str(&body)
.context("Failed to parse Maven Central response")?;
let docs = json["response"]["docs"]
.as_array()
.context("Invalid response format from Maven Central")?;
let mut results = Vec::new();
for doc in docs {
let group = doc["g"].as_str().unwrap_or("?");
let artifact = doc["a"].as_str().unwrap_or("?");
let version = doc["latestVersion"].as_str().unwrap_or("?");
let timestamp = doc["timestamp"].as_i64().unwrap_or(0);
let date = if timestamp > 0 {
let secs = timestamp / 1000;
chrono::DateTime::from_timestamp(secs, 0)
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_else(|| "?".to_string())
} else {
"?".to_string()
};
results.push(MavenCentralResult {
group_id: group.to_string(),
artifact_id: artifact.to_string(),
version: version.to_string(),
updated: date,
});
}
Ok(results)
}
#[derive(Debug, Clone)]
pub struct MavenCentralResult {
pub group_id: String,
pub artifact_id: String,
pub version: String,
pub updated: String,
}
impl MavenCentralResult {
pub fn to_coordinates(&self) -> String {
format!("{}:{}:{}", self.group_id, self.artifact_id, self.version)
}
}
pub fn fetch_all_versions(group_id: &str, artifact_id: &str) -> Result<Vec<String>> {
let query = format!("g:{} AND a:{}",
urlencoding::encode(group_id),
urlencoding::encode(artifact_id)
);
let url = format!(
"https://search.maven.org/solrsearch/select?q={}&rows=100&wt=json&sort=version desc",
urlencoding::encode(&query)
);
let response = ureq::get(&url)
.call()
.context("Failed to query Maven Central")?;
let body = response.into_string()
.context("Failed to read response from Maven Central")?;
let json: serde_json::Value = serde_json::from_str(&body)
.context("Failed to parse Maven Central response")?;
let docs = json["response"]["docs"]
.as_array()
.context("Invalid response format from Maven Central")?;
let mut versions: Vec<String> = docs.iter()
.filter_map(|doc| {
doc["v"].as_str().map(|v| v.to_string())
})
.collect();
versions.sort_by(|a, b| {
use crate::common::version::compare_versions;
use std::cmp::Ordering;
match compare_versions(b, a) {
x if x < 0 => Ordering::Less,
x if x > 0 => Ordering::Greater,
_ => Ordering::Equal,
}
});
versions.dedup();
Ok(versions)
}
pub fn fetch_package_info(group_id: &str, artifact_id: &str) -> Result<PackageInfo> {
let query = format!("g:{} AND a:{}",
urlencoding::encode(group_id),
urlencoding::encode(artifact_id)
);
let url = format!(
"https://search.maven.org/solrsearch/select?q={}&rows=1&wt=json",
urlencoding::encode(&query)
);
let response = ureq::get(&url)
.call()
.context("Failed to query Maven Central")?;
let body = response.into_string()
.context("Failed to read response from Maven Central")?;
let json: serde_json::Value = serde_json::from_str(&body)
.context("Failed to parse Maven Central response")?;
let docs = json["response"]["docs"]
.as_array()
.context("Invalid response format from Maven Central")?;
if docs.is_empty() {
return Err(anyhow::anyhow!(
"Package {group_id}:{artifact_id} not found on Maven Central"
));
}
let doc = &docs[0];
let latest_version = doc["latestVersion"]
.as_str()
.or_else(|| doc["v"].as_str())
.unwrap_or("?")
.to_string();
let timestamp = doc["timestamp"].as_i64().unwrap_or(0);
let updated = if timestamp > 0 {
let secs = timestamp / 1000;
chrono::DateTime::from_timestamp(secs, 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| "?".to_string())
} else {
"?".to_string()
};
let all_versions = fetch_all_versions(group_id, artifact_id)
.unwrap_or_default();
Ok(PackageInfo {
group_id: group_id.to_string(),
artifact_id: artifact_id.to_string(),
latest_version,
updated,
all_versions,
})
}
#[derive(Debug, Clone)]
pub struct PackageInfo {
pub group_id: String,
pub artifact_id: String,
pub latest_version: String,
pub updated: String,
pub all_versions: Vec<String>,
}