zv 0.11.0

Ziglang Version Manager and Project Starter
use crate::app::constants::ZLS_SELECT_VERSION_ENDPOINT;
use crate::app::network::create_client;
use crate::{NetErr, ZvError};
use color_eyre::eyre::eyre;
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct ZlsArtifact {
    pub tarball: String,
    pub shasum: String,
    pub size: u64,
}

#[derive(Debug, Clone)]
pub struct ZlsRelease {
    pub version: String,
    pub date: String,
    pub per_target: HashMap<String, ZlsArtifact>,
}

impl ZlsRelease {
    pub fn artifact_for_target(&self, target: &str) -> Option<&ZlsArtifact> {
        self.per_target.get(target)
    }
}

#[derive(Debug, Deserialize)]
struct SelectVersionResponse {
    version: String,
    date: String,
    #[serde(flatten)]
    targets: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Deserialize)]
struct ArtifactDto {
    tarball: String,
    shasum: String,
    #[serde(deserialize_with = "deserialize_size")]
    size: u64,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum SizeValue {
    Number(u64),
    String(String),
}

fn deserialize_size<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = SizeValue::deserialize(deserializer)?;
    match value {
        SizeValue::Number(value) => Ok(value),
        SizeValue::String(value) => value.parse::<u64>().map_err(serde::de::Error::custom),
    }
}

pub async fn select_version(zig_version: &str) -> Result<ZlsRelease, ZvError> {
    let client = create_client()?;
    let response = client
        .get(ZLS_SELECT_VERSION_ENDPOINT)
        .query(&[("zig_version", zig_version), ("compatibility", "full")])
        .send()
        .await
        .map_err(NetErr::Reqwest)
        .map_err(ZvError::NetworkError)?;

    if !response.status().is_success() {
        return Err(ZvError::NetworkError(NetErr::HTTP(response.status())));
    }

    let body: SelectVersionResponse = response
        .json()
        .await
        .map_err(NetErr::Reqwest)
        .map_err(ZvError::NetworkError)?;

    let mut per_target = HashMap::new();
    for (key, value) in body.targets {
        match serde_json::from_value::<ArtifactDto>(value) {
            Ok(artifact) => {
                per_target.insert(
                    key,
                    ZlsArtifact {
                        tarball: artifact.tarball,
                        shasum: artifact.shasum,
                        size: artifact.size,
                    },
                );
            }
            Err(err) => {
                tracing::warn!(
                    target: "zv::network::zls",
                    artifact_target = %key,
                    "Skipping malformed ZLS artifact in select-version response: {err}"
                );
            }
        }
    }

    if per_target.is_empty() {
        return Err(ZvError::General(eyre!(
            "ZLS select-version response did not include any target artifacts"
        )));
    }

    Ok(ZlsRelease {
        version: body.version,
        date: body.date,
        per_target,
    })
}