Skip to content

Commit 0a305bc

Browse files
committed
Implement ReleaseTracks
This will add `release_tracks` information to the meta field for paginated versions. This is necessary for pagination because it only loads a portion of versions per page, while `release_tracks` may require all sorted versions to determine their values. Therefore, we also need to calculate these on the server side.
1 parent f1b6235 commit 0a305bc

File tree

1 file changed

+186
-1
lines changed

1 file changed

+186
-1
lines changed

src/controllers/krate/versions.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ fn list_by_date(
133133

134134
Ok(PaginatedVersionsAndPublishers {
135135
data,
136-
meta: ResponseMeta { total, next_page },
136+
meta: ResponseMeta {
137+
total,
138+
next_page,
139+
release_tracks: None,
140+
},
137141
})
138142
}
139143

@@ -233,6 +237,7 @@ fn list_by_semver(
233237
meta: ResponseMeta {
234238
total: total as i64,
235239
next_page,
240+
release_tracks: None,
236241
},
237242
})
238243
}
@@ -302,4 +307,184 @@ struct PaginatedVersionsAndPublishers {
302307
struct ResponseMeta {
303308
total: i64,
304309
next_page: Option<String>,
310+
#[serde(skip_serializing_if = "Option::is_none")]
311+
release_tracks: Option<ReleaseTracks>,
312+
}
313+
314+
#[derive(Debug, Eq, PartialEq, Serialize)]
315+
struct ReleaseTracks(IndexMap<ReleaseSlug, HighestSemver>);
316+
317+
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
318+
enum ReleaseSlug {
319+
MajorX(u64),
320+
Minor(u64),
321+
Major(u64),
322+
}
323+
324+
#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
325+
struct HighestSemver {
326+
highest: semver::Version,
327+
}
328+
329+
impl ReleaseTracks {
330+
// Return the release tracks based on a sorted semver versions iterator (in descending order).
331+
// **Remember to** filter out yanked versions manually before calling this function.
332+
pub fn from_sorted_semver_iter<'a, I>(versions: I) -> Self
333+
where
334+
I: Iterator<Item = &'a semver::Version>,
335+
{
336+
let mut map = IndexMap::new();
337+
for num in versions.filter(|num| num.pre.is_empty()) {
338+
let key = ReleaseSlug::from_semver(num);
339+
let prev = map.last();
340+
if prev.filter(|&(k, _)| *k == key).is_none() {
341+
map.insert(
342+
key,
343+
HighestSemver {
344+
highest: num.clone(),
345+
},
346+
);
347+
}
348+
}
349+
350+
Self(map)
351+
}
352+
}
353+
354+
impl ReleaseSlug {
355+
pub fn from_semver(version: &semver::Version) -> Self {
356+
if version.major >= 100 {
357+
Self::Major(version.major)
358+
} else if version.major == 0 {
359+
Self::Minor(version.minor)
360+
} else {
361+
Self::MajorX(version.major)
362+
}
363+
}
364+
}
365+
366+
impl std::fmt::Display for ReleaseSlug {
367+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368+
match self {
369+
Self::MajorX(major) => write!(f, "{major}.x"),
370+
Self::Minor(minor) => write!(f, "0.{minor}"),
371+
Self::Major(major) => write!(f, "{major}"),
372+
}
373+
}
374+
}
375+
376+
impl serde::Serialize for ReleaseSlug {
377+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
378+
where
379+
S: serde::Serializer,
380+
Self: std::fmt::Display,
381+
{
382+
serializer.collect_str(self)
383+
}
384+
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::{HighestSemver, ReleaseSlug, ReleaseTracks};
389+
use indexmap::IndexMap;
390+
391+
#[track_caller]
392+
fn version(str: &str) -> semver::Version {
393+
semver::Version::parse(str).unwrap()
394+
}
395+
396+
#[test]
397+
fn release_tracks_empty() {
398+
let versions = [];
399+
assert_eq!(
400+
ReleaseTracks::from_sorted_semver_iter(versions.into_iter()),
401+
ReleaseTracks(IndexMap::new())
402+
);
403+
}
404+
405+
#[test]
406+
fn release_tracks_prerelease() {
407+
let versions = [version("1.0.0-beta.5")];
408+
assert_eq!(
409+
ReleaseTracks::from_sorted_semver_iter(versions.iter()),
410+
ReleaseTracks(IndexMap::new())
411+
);
412+
}
413+
414+
#[test]
415+
fn release_tracks_multiple() {
416+
let versions = [
417+
"100.1.1",
418+
"100.1.0",
419+
"1.3.5",
420+
"1.2.5",
421+
"1.1.5",
422+
"0.4.0-rc.1",
423+
"0.3.23",
424+
"0.3.22",
425+
"0.3.21-pre.0",
426+
"0.3.20",
427+
"0.3.3",
428+
"0.3.2",
429+
"0.3.1",
430+
"0.3.0",
431+
"0.2.1",
432+
"0.2.0",
433+
"0.1.2",
434+
"0.1.1",
435+
]
436+
.map(version);
437+
438+
let release_tracks = ReleaseTracks::from_sorted_semver_iter(versions.iter());
439+
assert_eq!(
440+
release_tracks,
441+
ReleaseTracks(IndexMap::from([
442+
(
443+
ReleaseSlug::Major(100),
444+
HighestSemver {
445+
highest: version("100.1.1")
446+
}
447+
),
448+
(
449+
ReleaseSlug::MajorX(1),
450+
HighestSemver {
451+
highest: version("1.3.5")
452+
}
453+
),
454+
(
455+
ReleaseSlug::Minor(3),
456+
HighestSemver {
457+
highest: version("0.3.23")
458+
}
459+
),
460+
(
461+
ReleaseSlug::Minor(2),
462+
HighestSemver {
463+
highest: version("0.2.1")
464+
}
465+
),
466+
(
467+
ReleaseSlug::Minor(1),
468+
HighestSemver {
469+
highest: version("0.1.2")
470+
}
471+
),
472+
]))
473+
);
474+
475+
let json = serde_json::from_str::<serde_json::Value>(
476+
&serde_json::to_string(&release_tracks).unwrap(),
477+
)
478+
.unwrap();
479+
assert_eq!(
480+
json,
481+
json!({
482+
"100": { "highest": "100.1.1" },
483+
"1.x": { "highest": "1.3.5" },
484+
"0.3": { "highest": "0.3.23" },
485+
"0.2": { "highest": "0.2.1" },
486+
"0.1": { "highest": "0.1.2" }
487+
})
488+
);
489+
}
305490
}

0 commit comments

Comments
 (0)