Skip to content

Commit e170895

Browse files
committed
controllers/krate/versions: Implement release_tracks meta for pagination, sorted by date
1 parent e5ed924 commit e170895

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

src/controllers/krate/versions.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
use axum::extract::Path;
44
use axum::Json;
55
use diesel::connection::DefaultLoadingMode;
6+
use diesel::dsl::not;
67
use diesel::prelude::*;
78
use diesel_async::async_connection_wrapper::AsyncConnectionWrapper;
89
use http::request::Parts;
9-
use indexmap::IndexMap;
10+
use indexmap::{IndexMap, IndexSet};
1011
use serde_json::Value;
1112
use std::cmp::Reverse;
1213

@@ -95,6 +96,7 @@ fn list_by_date(
9596
.select((versions::all_columns, users::all_columns.nullable()))
9697
.into_boxed();
9798

99+
let mut release_tracks = None;
98100
if let Some(options) = options {
99101
assert!(
100102
!matches!(&options.page, Page::Numeric(_)),
@@ -109,6 +111,23 @@ fn list_by_date(
109111
)
110112
}
111113
query = query.limit(options.per_page);
114+
115+
let mut sorted_versions = IndexSet::new();
116+
for result in versions::table
117+
.filter(versions::crate_id.eq(crate_id))
118+
.filter(not(versions::yanked))
119+
.select(versions::num)
120+
.load_iter::<String, DefaultLoadingMode>(conn)?
121+
{
122+
let Ok(semver) = semver::Version::parse(&result?) else {
123+
continue;
124+
};
125+
sorted_versions.insert(semver);
126+
}
127+
sorted_versions.sort_unstable_by(|a, b| b.cmp(a));
128+
release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
129+
sorted_versions.iter(),
130+
));
112131
}
113132

114133
query = query.order((versions::created_at.desc(), versions::id.desc()));
@@ -136,7 +155,7 @@ fn list_by_date(
136155
meta: ResponseMeta {
137156
total,
138157
next_page,
139-
release_tracks: None,
158+
release_tracks,
140159
},
141160
})
142161
}

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": {"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.1").yanked(true))
225+
.version(VersionBuilder::new("1.0.0").rust_version("1.64"))
226+
.version("0.5.0")
227+
.expect_build(conn);
228+
// Make version 1.0.0 mimic a version published before we started recording who published
229+
// versions
230+
let none: Option<i32> = None;
231+
update(versions::table)
232+
.filter(versions::num.eq("1.0.0"))
233+
.set(versions::published_by.eq(none))
234+
.execute(conn)
235+
.unwrap();
236+
});
237+
238+
let url = "/api/v1/crates/foo_versions/versions";
239+
let expects = ["0.5.0", "1.0.0", "0.5.1"];
240+
let release_tracks = Some(json!({
241+
"1": {"highest": "1.0.0"},
242+
"0.5": {"highest": "0.5.0"}
243+
}));
244+
245+
// per_page larger than the number of versions
246+
let json: VersionList = anon
247+
.get_with_query(url, "per_page=10&sort=date")
248+
.await
249+
.good();
250+
assert_eq!(nums(&json.versions), expects);
251+
assert_eq!(json.meta.total as usize, expects.len());
252+
assert_eq!(json.meta.release_tracks, release_tracks);
253+
254+
let json: VersionList = anon
255+
.get_with_query(url, "per_page=1&sort=date")
256+
.await
257+
.good();
258+
assert_eq!(nums(&json.versions), expects[0..1]);
259+
assert_eq!(json.meta.total as usize, expects.len());
260+
assert_eq!(json.meta.release_tracks, release_tracks);
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..]);
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)