simit 0.17.3

Semver-aware git commit helper for Rust projects
Documentation
use crate::config::{ResolvedScoop, ScoopArchSet};

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Architecture {
    X64,
    Arm64,
}

impl Architecture {
    pub fn parse(value: &str) -> Result<Self, String> {
        match value {
            "x64" | "64bit" => Ok(Self::X64),
            "arm64" => Ok(Self::Arm64),
            other => Err(format!(
                "unknown architecture {other}; expected x64 or arm64"
            )),
        }
    }

    pub fn key(self) -> &'static str {
        match self {
            Self::X64 => "x64",
            Self::Arm64 => "arm64",
        }
    }

    pub fn manifest_key(self) -> &'static str {
        match self {
            Self::X64 => "64bit",
            Self::Arm64 => "arm64",
        }
    }

    pub fn arch(self) -> &'static str {
        match self {
            Self::X64 => "x86_64",
            Self::Arm64 => "aarch64",
        }
    }
}

#[derive(Debug, Default)]
pub struct ScoopChecksums {
    pub x64: Option<String>,
    pub arm64: Option<String>,
}

impl ScoopChecksums {
    pub fn all_placeholder() -> Self {
        Self {
            x64: Some(placeholder_hash()),
            arm64: Some(placeholder_hash()),
        }
    }

    pub fn set(&mut self, architecture: Architecture, value: String) {
        *self.slot_mut(architecture) = Some(value);
    }

    fn get(&self, architecture: Architecture) -> Option<&str> {
        match architecture {
            Architecture::X64 => self.x64.as_deref(),
            Architecture::Arm64 => self.arm64.as_deref(),
        }
    }

    fn slot_mut(&mut self, architecture: Architecture) -> &mut Option<String> {
        match architecture {
            Architecture::X64 => &mut self.x64,
            Architecture::Arm64 => &mut self.arm64,
        }
    }
}

pub fn render(resolved: &ResolvedScoop, version: &str, checksums: &ScoopChecksums) -> String {
    let architectures = enabled_architectures(&resolved.architectures);
    let mut lines = vec![
        "{".to_owned(),
        format!("  \"version\": {},", json_string(version)),
        format!("  \"description\": {},", json_string(&resolved.description)),
        format!("  \"homepage\": {},", json_string(&resolved.homepage)),
        format!("  \"license\": {},", json_string(&resolved.license)),
        "  \"architecture\": {".to_owned(),
    ];

    for (index, architecture) in architectures.iter().copied().enumerate() {
        let arch_comma = if index + 1 == architectures.len() {
            ""
        } else {
            ","
        };
        lines.push(format!("    \"{}\": {{", architecture.manifest_key()));
        lines.push(format!(
            "      \"url\": {},",
            json_string(&archive_url(resolved, version, architecture))
        ));
        lines.push(format!(
            "      \"hash\": {},",
            json_string(checksums.get(architecture).unwrap_or(PLACEHOLDER_HASH))
        ));
        lines.push(format!("      \"bin\": {}", render_bin(&resolved.binaries)));
        lines.push(format!("    }}{arch_comma}"));
    }

    lines.push("  }".to_owned());
    lines.push("}".to_owned());
    lines.push(String::new());
    lines.join("\n")
}

pub fn archive_file_name(
    resolved: &ResolvedScoop,
    version: &str,
    architecture: Architecture,
) -> String {
    resolved
        .archive_pattern
        .replace("{name}", &resolved.name)
        .replace("{version}", version)
        .replace("{arch}", architecture.arch())
}

pub fn archive_url(resolved: &ResolvedScoop, version: &str, architecture: Architecture) -> String {
    format!(
        "https://codeberg.org/{}/releases/download/{}/{}",
        resolved.download_repo,
        version,
        archive_file_name(resolved, version, architecture)
    )
}

pub fn enabled_architectures(architectures: &ScoopArchSet) -> Vec<Architecture> {
    [
        (Architecture::X64, architectures.x64),
        (Architecture::Arm64, architectures.arm64),
    ]
    .into_iter()
    .filter_map(|(architecture, enabled)| enabled.then_some(architecture))
    .collect()
}

fn render_bin(binaries: &[String]) -> String {
    if let [binary] = binaries {
        json_string(binary)
    } else {
        let values = binaries
            .iter()
            .map(|binary| json_string(binary))
            .collect::<Vec<_>>();
        format!("[{}]", values.join(", "))
    }
}

fn placeholder_hash() -> String {
    PLACEHOLDER_HASH.to_owned()
}

fn json_string(value: &str) -> String {
    serde_json::to_string(value).expect("serializing string to JSON cannot fail")
}

const PLACEHOLDER_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";