use crate::error::{KopiError, Result};
use crate::models::api::Package;
use crate::paths::shared::sanitize_segment;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PackageKind {
Jdk,
Jre,
}
impl PackageKind {
fn slug_segment(self) -> &'static str {
match self {
PackageKind::Jdk => "jdk",
PackageKind::Jre => "jre",
}
}
pub fn try_from_str(value: &str) -> Result<Self> {
match value.to_ascii_lowercase().as_str() {
"jdk" => Ok(PackageKind::Jdk),
"jre" => Ok(PackageKind::Jre),
other => Err(KopiError::ValidationError(format!(
"Unsupported package type '{other}'"
))),
}
}
}
impl fmt::Display for PackageKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.slug_segment())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageCoordinate {
distribution: String,
major_version: u32,
package_kind: PackageKind,
architecture: Option<String>,
operating_system: Option<String>,
libc_variant: Option<String>,
javafx_bundled: bool,
variant_tags: Vec<String>,
}
impl PackageCoordinate {
pub fn new<S: Into<String>>(
distribution: S,
major_version: u32,
package_kind: PackageKind,
) -> Self {
Self {
distribution: distribution.into(),
major_version,
package_kind,
architecture: None,
operating_system: None,
libc_variant: None,
javafx_bundled: false,
variant_tags: Vec::new(),
}
}
pub fn distribution(&self) -> &str {
&self.distribution
}
pub fn major_version(&self) -> u32 {
self.major_version
}
pub fn package_kind(&self) -> PackageKind {
self.package_kind
}
pub fn architecture(&self) -> Option<&str> {
self.architecture.as_deref()
}
pub fn operating_system(&self) -> Option<&str> {
self.operating_system.as_deref()
}
pub fn libc_variant(&self) -> Option<&str> {
self.libc_variant.as_deref()
}
pub fn variant_tags(&self) -> &[String] {
&self.variant_tags
}
pub fn javafx_bundled(&self) -> bool {
self.javafx_bundled
}
pub fn with_architecture<S: Into<String>>(mut self, architecture: Option<S>) -> Self {
self.architecture = architecture.map(|value| value.into());
self
}
pub fn with_operating_system<S: Into<String>>(mut self, operating_system: Option<S>) -> Self {
self.operating_system = operating_system.map(|value| value.into());
self
}
pub fn with_libc_variant<S: Into<String>>(mut self, libc_variant: Option<S>) -> Self {
self.libc_variant = libc_variant.map(|value| value.into());
self
}
pub fn with_javafx(mut self, javafx_bundled: bool) -> Self {
self.javafx_bundled = javafx_bundled;
self
}
pub fn with_variant_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.variant_tags = tags.into_iter().map(|tag| tag.into()).collect();
self
}
pub fn slug(&self) -> String {
let mut segments = Vec::new();
if let Some(segment) = sanitize_segment(&self.distribution) {
segments.push(segment);
}
segments.push(self.major_version.to_string());
segments.push(self.package_kind.slug_segment().to_string());
if let Some(architecture) = self
.architecture
.as_ref()
.and_then(|value| sanitize_segment(value))
{
segments.push(architecture);
}
if let Some(os) = self
.operating_system
.as_ref()
.and_then(|value| sanitize_segment(value))
{
segments.push(os);
}
if let Some(libc) = self
.libc_variant
.as_ref()
.and_then(|value| sanitize_segment(value))
{
segments.push(libc);
}
let mut extras: Vec<String> = self
.variant_tags
.iter()
.filter_map(|value| sanitize_segment(value))
.collect();
extras.sort();
extras.dedup();
segments.extend(extras);
if self.javafx_bundled {
segments.push("javafx".to_string());
}
segments.join("-")
}
pub fn try_from_package(package: &Package) -> Result<Self> {
let kind = PackageKind::try_from_str(&package.package_type)?;
let variants = build_variant_tags(package);
Ok(
Self::new(package.distribution.clone(), package.major_version, kind)
.with_architecture(package.architecture.clone())
.with_operating_system(Some(package.operating_system.clone()))
.with_libc_variant(package.lib_c_type.clone())
.with_javafx(package.javafx_bundled)
.with_variant_tags(variants),
)
}
}
fn build_variant_tags(package: &Package) -> Vec<String> {
let mut tags = Vec::new();
if let Some(term) = &package.term_of_support {
tags.push(term.clone());
}
if let Some(status) = &package.release_status {
tags.push(status.clone());
}
if package.latest_build_available.unwrap_or(false) {
tags.push("latest".to_string());
}
tags
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_package() -> Package {
Package {
id: "pkg-id".to_string(),
archive_type: "tar.gz".to_string(),
distribution: "Temurin".to_string(),
major_version: 21,
java_version: "21.0.2".to_string(),
distribution_version: "21.0.2".to_string(),
jdk_version: 21,
directly_downloadable: true,
filename: "openjdk.tar.gz".to_string(),
links: crate::models::api::Links {
pkg_download_redirect: "https://example.com".to_string(),
pkg_info_uri: Some("https://example.com/info".to_string()),
},
free_use_in_production: true,
tck_tested: "yes".to_string(),
size: 1024,
operating_system: "linux".to_string(),
architecture: Some("x64".to_string()),
lib_c_type: Some("gnu".to_string()),
package_type: "JDK".to_string(),
javafx_bundled: true,
term_of_support: Some("lts".to_string()),
release_status: Some("ga".to_string()),
latest_build_available: Some(true),
}
}
#[test]
fn slug_includes_expected_segments() {
let coordinate = PackageCoordinate::new("Temurin", 21, PackageKind::Jdk)
.with_architecture(Some("x64"))
.with_javafx(true);
assert_eq!(coordinate.slug(), "temurin-21-jdk-x64-javafx");
}
#[test]
fn slug_is_deterministic_with_variants() {
let coordinate = PackageCoordinate::new("Temurin", 21, PackageKind::Jdk)
.with_architecture(Some("x64"))
.with_operating_system(Some("Linux"))
.with_libc_variant(Some("gnu"))
.with_variant_tags(["ga", "lts", "ga"])
.with_javafx(false);
assert_eq!(coordinate.slug(), "temurin-21-jdk-x64-linux-gnu-ga-lts");
}
#[test]
fn try_from_package_populates_fields() {
let package = sample_package();
let coordinate = PackageCoordinate::try_from_package(&package).unwrap();
assert_eq!(coordinate.distribution(), "Temurin");
assert_eq!(coordinate.major_version(), 21);
assert_eq!(coordinate.package_kind(), PackageKind::Jdk);
assert_eq!(coordinate.architecture(), Some("x64"));
assert_eq!(coordinate.operating_system(), Some("linux"));
assert_eq!(coordinate.libc_variant(), Some("gnu"));
assert!(coordinate.javafx_bundled());
assert!(coordinate.variant_tags().iter().any(|tag| tag == "lts"));
}
#[test]
fn sanitize_segment_removes_duplicates_and_case() {
assert_eq!(sanitize_segment(" Tem urin "), Some("tem-urin".to_string()));
assert_eq!(sanitize_segment("***"), None);
}
}