1use crate::error::{KopiError, Result};
16use crate::models::api::Package;
17use crate::paths::shared::sanitize_segment;
18use std::fmt;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum PackageKind {
23 Jdk,
24 Jre,
25}
26
27impl PackageKind {
28 fn slug_segment(self) -> &'static str {
29 match self {
30 PackageKind::Jdk => "jdk",
31 PackageKind::Jre => "jre",
32 }
33 }
34
35 pub fn try_from_str(value: &str) -> Result<Self> {
36 match value.to_ascii_lowercase().as_str() {
37 "jdk" => Ok(PackageKind::Jdk),
38 "jre" => Ok(PackageKind::Jre),
39 other => Err(KopiError::ValidationError(format!(
40 "Unsupported package type '{other}'"
41 ))),
42 }
43 }
44}
45
46impl fmt::Display for PackageKind {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "{}", self.slug_segment())
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct PackageCoordinate {
55 distribution: String,
56 major_version: u32,
57 package_kind: PackageKind,
58 architecture: Option<String>,
59 operating_system: Option<String>,
60 libc_variant: Option<String>,
61 javafx_bundled: bool,
62 variant_tags: Vec<String>,
63}
64
65impl PackageCoordinate {
66 pub fn new<S: Into<String>>(
68 distribution: S,
69 major_version: u32,
70 package_kind: PackageKind,
71 ) -> Self {
72 Self {
73 distribution: distribution.into(),
74 major_version,
75 package_kind,
76 architecture: None,
77 operating_system: None,
78 libc_variant: None,
79 javafx_bundled: false,
80 variant_tags: Vec::new(),
81 }
82 }
83
84 pub fn distribution(&self) -> &str {
85 &self.distribution
86 }
87
88 pub fn major_version(&self) -> u32 {
89 self.major_version
90 }
91
92 pub fn package_kind(&self) -> PackageKind {
93 self.package_kind
94 }
95
96 pub fn architecture(&self) -> Option<&str> {
97 self.architecture.as_deref()
98 }
99
100 pub fn operating_system(&self) -> Option<&str> {
101 self.operating_system.as_deref()
102 }
103
104 pub fn libc_variant(&self) -> Option<&str> {
105 self.libc_variant.as_deref()
106 }
107
108 pub fn variant_tags(&self) -> &[String] {
109 &self.variant_tags
110 }
111
112 pub fn javafx_bundled(&self) -> bool {
113 self.javafx_bundled
114 }
115
116 pub fn with_architecture<S: Into<String>>(mut self, architecture: Option<S>) -> Self {
117 self.architecture = architecture.map(|value| value.into());
118 self
119 }
120
121 pub fn with_operating_system<S: Into<String>>(mut self, operating_system: Option<S>) -> Self {
122 self.operating_system = operating_system.map(|value| value.into());
123 self
124 }
125
126 pub fn with_libc_variant<S: Into<String>>(mut self, libc_variant: Option<S>) -> Self {
127 self.libc_variant = libc_variant.map(|value| value.into());
128 self
129 }
130
131 pub fn with_javafx(mut self, javafx_bundled: bool) -> Self {
132 self.javafx_bundled = javafx_bundled;
133 self
134 }
135
136 pub fn with_variant_tags<I, S>(mut self, tags: I) -> Self
137 where
138 I: IntoIterator<Item = S>,
139 S: Into<String>,
140 {
141 self.variant_tags = tags.into_iter().map(|tag| tag.into()).collect();
142 self
143 }
144
145 pub fn slug(&self) -> String {
147 let mut segments = Vec::new();
148
149 if let Some(segment) = sanitize_segment(&self.distribution) {
150 segments.push(segment);
151 }
152
153 segments.push(self.major_version.to_string());
154 segments.push(self.package_kind.slug_segment().to_string());
155
156 if let Some(architecture) = self
157 .architecture
158 .as_ref()
159 .and_then(|value| sanitize_segment(value))
160 {
161 segments.push(architecture);
162 }
163
164 if let Some(os) = self
165 .operating_system
166 .as_ref()
167 .and_then(|value| sanitize_segment(value))
168 {
169 segments.push(os);
170 }
171
172 if let Some(libc) = self
173 .libc_variant
174 .as_ref()
175 .and_then(|value| sanitize_segment(value))
176 {
177 segments.push(libc);
178 }
179
180 let mut extras: Vec<String> = self
181 .variant_tags
182 .iter()
183 .filter_map(|value| sanitize_segment(value))
184 .collect();
185 extras.sort();
186 extras.dedup();
187 segments.extend(extras);
188
189 if self.javafx_bundled {
190 segments.push("javafx".to_string());
191 }
192
193 segments.join("-")
194 }
195
196 pub fn try_from_package(package: &Package) -> Result<Self> {
198 let kind = PackageKind::try_from_str(&package.package_type)?;
199 let variants = build_variant_tags(package);
200
201 Ok(
202 Self::new(package.distribution.clone(), package.major_version, kind)
203 .with_architecture(package.architecture.clone())
204 .with_operating_system(Some(package.operating_system.clone()))
205 .with_libc_variant(package.lib_c_type.clone())
206 .with_javafx(package.javafx_bundled)
207 .with_variant_tags(variants),
208 )
209 }
210}
211
212fn build_variant_tags(package: &Package) -> Vec<String> {
213 let mut tags = Vec::new();
214
215 if let Some(term) = &package.term_of_support {
216 tags.push(term.clone());
217 }
218
219 if let Some(status) = &package.release_status {
220 tags.push(status.clone());
221 }
222
223 if package.latest_build_available.unwrap_or(false) {
224 tags.push("latest".to_string());
225 }
226
227 tags
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn sample_package() -> Package {
235 Package {
236 id: "pkg-id".to_string(),
237 archive_type: "tar.gz".to_string(),
238 distribution: "Temurin".to_string(),
239 major_version: 21,
240 java_version: "21.0.2".to_string(),
241 distribution_version: "21.0.2".to_string(),
242 jdk_version: 21,
243 directly_downloadable: true,
244 filename: "openjdk.tar.gz".to_string(),
245 links: crate::models::api::Links {
246 pkg_download_redirect: "https://example.com".to_string(),
247 pkg_info_uri: Some("https://example.com/info".to_string()),
248 },
249 free_use_in_production: true,
250 tck_tested: "yes".to_string(),
251 size: 1024,
252 operating_system: "linux".to_string(),
253 architecture: Some("x64".to_string()),
254 lib_c_type: Some("gnu".to_string()),
255 package_type: "JDK".to_string(),
256 javafx_bundled: true,
257 term_of_support: Some("lts".to_string()),
258 release_status: Some("ga".to_string()),
259 latest_build_available: Some(true),
260 }
261 }
262
263 #[test]
264 fn slug_includes_expected_segments() {
265 let coordinate = PackageCoordinate::new("Temurin", 21, PackageKind::Jdk)
266 .with_architecture(Some("x64"))
267 .with_javafx(true);
268
269 assert_eq!(coordinate.slug(), "temurin-21-jdk-x64-javafx");
270 }
271
272 #[test]
273 fn slug_is_deterministic_with_variants() {
274 let coordinate = PackageCoordinate::new("Temurin", 21, PackageKind::Jdk)
275 .with_architecture(Some("x64"))
276 .with_operating_system(Some("Linux"))
277 .with_libc_variant(Some("gnu"))
278 .with_variant_tags(["ga", "lts", "ga"])
279 .with_javafx(false);
280
281 assert_eq!(coordinate.slug(), "temurin-21-jdk-x64-linux-gnu-ga-lts");
282 }
283
284 #[test]
285 fn try_from_package_populates_fields() {
286 let package = sample_package();
287 let coordinate = PackageCoordinate::try_from_package(&package).unwrap();
288
289 assert_eq!(coordinate.distribution(), "Temurin");
290 assert_eq!(coordinate.major_version(), 21);
291 assert_eq!(coordinate.package_kind(), PackageKind::Jdk);
292 assert_eq!(coordinate.architecture(), Some("x64"));
293 assert_eq!(coordinate.operating_system(), Some("linux"));
294 assert_eq!(coordinate.libc_variant(), Some("gnu"));
295 assert!(coordinate.javafx_bundled());
296 assert!(coordinate.variant_tags().iter().any(|tag| tag == "lts"));
297 }
298
299 #[test]
300 fn sanitize_segment_removes_duplicates_and_case() {
301 assert_eq!(sanitize_segment(" Tem urin "), Some("tem-urin".to_string()));
302 assert_eq!(sanitize_segment("***"), None);
303 }
304}