use crate::{config::Loader, fs::USER_AGENT};
use color_eyre::eyre::{self, Result, eyre};
use serde::{Deserialize, Serialize, Serializer};
const MODRINTH_BASE_URL: &str = "https://api.modrinth.com/v2";
fn as_json_array<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
let json = serde_json::to_string(&[value]).map_err(serde::ser::Error::custom)?;
serializer.serialize_str(&json)
}
#[derive(Debug, Serialize)]
struct VersionQuery<'a> {
#[serde(serialize_with = "as_json_array")]
loaders: &'a Loader,
#[serde(serialize_with = "as_json_array")]
game_versions: &'a str,
include_changelog: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VersionStatus {
Listed,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VersionType {
Release,
Beta,
Alpha,
}
impl VersionType {
const fn priority(&self) -> u8 {
match self {
Self::Release => 2,
Self::Beta => 1,
Self::Alpha => 0,
}
}
}
#[derive(Debug, Deserialize)]
pub struct VersionFileHashes {
pub sha512: String,
}
#[derive(Debug, Deserialize)]
pub struct VersionFile {
pub id: String,
pub hashes: VersionFileHashes,
pub url: String,
pub filename: String,
pub primary: bool,
}
#[derive(Debug, Deserialize)]
pub struct Version {
pub id: String,
pub project_id: String,
pub name: String,
pub game_versions: Vec<String>,
pub loaders: Vec<Loader>,
pub date_published: chrono::DateTime<chrono::Utc>,
pub status: VersionStatus,
#[allow(clippy::struct_field_names)]
pub version_type: VersionType,
pub files: Vec<VersionFile>,
}
pub async fn get_project_versions(project: &str) -> Result<Vec<Version>> {
let versions = reqwest::ClientBuilder::new()
.user_agent(USER_AGENT)
.build()?
.get(format!("{MODRINTH_BASE_URL}/project/{project}/version"))
.query(&[("include_changelog", "false")])
.send()
.await?
.error_for_status()?
.json::<Vec<Version>>()
.await?;
Ok(versions)
}
pub async fn get_latest_project_version(
project: &str,
loader: &Loader,
game_version: &str,
) -> Result<Version> {
let version = reqwest::ClientBuilder::new()
.user_agent(USER_AGENT)
.build()?
.get(format!("{MODRINTH_BASE_URL}/project/{project}/version"))
.query(&VersionQuery {
loaders: loader,
game_versions: game_version,
include_changelog: false,
})
.send()
.await?
.error_for_status()?
.json::<Vec<Version>>()
.await?
.into_iter()
.max_by_key(|item| (item.version_type.priority(), item.date_published))
.ok_or_else(|| {
eyre!(
"no versions of {} found for game version {} using {} loader (check your cari.json)",
project,
game_version,
loader
)
})?;
Ok(version)
}
pub async fn get_project_version(project: &str, version: &str, loader: &Loader) -> Result<Version> {
let version_data = reqwest::ClientBuilder::new()
.user_agent(USER_AGENT)
.build()?
.get(format!(
"{MODRINTH_BASE_URL}/project/{project}/version/{version}"
))
.send()
.await?
.error_for_status()?
.json::<Version>()
.await?;
if !version_data.loaders.contains(loader) {
eyre::bail!(
"{} ({}) does not support loader {}",
project,
version,
loader
);
}
Ok(version_data)
}