Skip to content

Commit 93dff91

Browse files
committed
controllers/krate/versions: Implement release_tracks meta for pagination, sorted by date
1 parent 879c142 commit 93dff91

File tree

2 files changed

+109
-2
lines changed

2 files changed

+109
-2
lines changed

src/controllers/krate/versions.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use diesel::connection::DefaultLoadingMode;
66
use diesel::prelude::*;
77
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
88
use http::request::Parts;
9-
use indexmap::IndexMap;
9+
use indexmap::{IndexMap, IndexSet};
1010
use serde_json::Value;
1111
use std::cmp::Reverse;
1212

@@ -95,6 +95,7 @@ fn list_by_date(
9595
.select((versions::all_columns, users::all_columns.nullable()))
9696
.into_boxed();
9797

98+
let mut release_tracks = None;
9899
if let Some(options) = options {
99100
assert!(
100101
!matches!(&options.page, Page::Numeric(_)),
@@ -109,6 +110,23 @@ fn list_by_date(
109110
)
110111
}
111112
query = query.limit(options.per_page);
113+
114+
let mut sorted_versions = IndexSet::new();
115+
for result in versions::table
116+
.filter(versions::crate_id.eq(crate_id))
117+
.filter(versions::yanked.ne(true))
118+
.select(versions::num)
119+
.load_iter::<String, DefaultLoadingMode>(conn)?
120+
{
121+
let Ok(semver) = semver::Version::parse(&result?) else {
122+
continue;
123+
};
124+
sorted_versions.insert(semver);
125+
}
126+
sorted_versions.sort_unstable_by(|a, b| b.cmp(a));
127+
release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
128+
sorted_versions.iter(),
129+
));
112130
}
113131

114132
query = query.order((versions::created_at.desc(), versions::id.desc()));
@@ -136,7 +154,7 @@ fn list_by_date(
136154
meta: ResponseMeta {
137155
total,
138156
next_page,
139-
release_tracks: None,
157+
release_tracks,
140158
},
141159
})
142160
}

src/tests/routes/crates/versions/list.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ async fn test_sorting() {
125125
for (json, expect) in resp.iter().zip(&expects) {
126126
assert_eq!(json.versions[0].num, *expect);
127127
assert_eq!(json.meta.total as usize, expects.len());
128+
assert_eq!(
129+
json.meta.release_tracks,
130+
Some(json!({"1.x": {"highest": "1.0.0"}}))
131+
);
128132
}
129133
assert_eq!(calls as usize, expects.len() + 1);
130134
}
@@ -211,6 +215,91 @@ async fn test_seek_based_pagination_semver_sorting() {
211215
assert_eq!(json.meta.release_tracks, release_tracks);
212216
}
213217

218+
#[tokio::test(flavor = "multi_thread")]
219+
async fn test_seek_based_pagination_date_sorting() {
220+
let (app, anon, user) = TestApp::init().with_user();
221+
let user = user.as_model();
222+
app.db(|conn| {
223+
CrateBuilder::new("foo_versions", user.id)
224+
.version(VersionBuilder::new("0.5.2").yanked(true))
225+
.version("0.5.1")
226+
.version(VersionBuilder::new("1.0.0").rust_version("1.64"))
227+
.version("0.5.0")
228+
.expect_build(conn);
229+
// Make version 1.0.0 mimic a version published before we started recording who published
230+
// versions
231+
let none: Option<i32> = None;
232+
update(versions::table)
233+
.filter(versions::num.eq("1.0.0"))
234+
.set(versions::published_by.eq(none))
235+
.execute(conn)
236+
.unwrap();
237+
});
238+
239+
let url = "/api/v1/crates/foo_versions/versions";
240+
let expects = ["0.5.0", "1.0.0", "0.5.1", "0.5.2"];
241+
let release_tracks = Some(json!({
242+
"1.x": {"highest": "1.0.0"},
243+
"0.5": {"highest": "0.5.1"}
244+
}));
245+
246+
// per_page larger than the number of versions
247+
let json: VersionList = anon
248+
.get_with_query(url, "per_page=10&sort=date")
249+
.await
250+
.good();
251+
assert_eq!(nums(&json.versions), expects);
252+
assert_eq!(json.meta.total as usize, expects.len());
253+
assert_eq!(json.meta.release_tracks, release_tracks);
254+
255+
let json: VersionList = anon
256+
.get_with_query(url, "per_page=1&sort=date")
257+
.await
258+
.good();
259+
assert_eq!(nums(&json.versions), expects[0..1]);
260+
assert_eq!(json.meta.total as usize, expects.len());
261+
262+
let seek = json
263+
.meta
264+
.next_page
265+
.map(|s| s.split_once("seek=").unwrap().1.to_owned())
266+
.map(|p| p.split_once('&').map(|t| t.0.to_owned()).unwrap_or(p))
267+
.unwrap();
268+
269+
// per_page larger than the number of remain versions
270+
let json: VersionList = anon
271+
.get_with_query(url, &format!("per_page=5&sort=date&seek={seek}"))
272+
.await
273+
.good();
274+
assert_eq!(nums(&json.versions), expects[1..]);
275+
assert!(json.meta.next_page.is_none());
276+
assert_eq!(json.meta.total as usize, expects.len());
277+
assert_eq!(json.meta.release_tracks, release_tracks);
278+
279+
// per_page euqal to the number of remain versions
280+
let json: VersionList = anon
281+
.get_with_query(url, &format!("per_page=2&sort=date&seek={seek}"))
282+
.await
283+
.good();
284+
assert_eq!(nums(&json.versions), expects[1..3]);
285+
assert!(json.meta.next_page.is_some());
286+
assert_eq!(json.meta.total as usize, expects.len());
287+
assert_eq!(json.meta.release_tracks, release_tracks);
288+
289+
// A decodable seek value, WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K ([1728561992734673,5]), but doesn't actually exist
290+
let json: VersionList = anon
291+
.get_with_query(
292+
url,
293+
"per_page=10&sort=date&seek=WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K",
294+
)
295+
.await
296+
.good();
297+
assert_eq!(json.versions.len(), 0);
298+
assert!(json.meta.next_page.is_none());
299+
assert_eq!(json.meta.total, 0);
300+
assert_eq!(json.meta.release_tracks, release_tracks);
301+
}
302+
214303
#[tokio::test(flavor = "multi_thread")]
215304
async fn invalid_seek_parameter() {
216305
let (app, anon, user) = TestApp::init().with_user();

0 commit comments

Comments
 (0)