Skip to content

Commit 879c142

Browse files
committed
controllers/krate/versions: Implement release_tracks meta for pagination, sorted by semver
1 parent 028832f commit 879c142

File tree

2 files changed

+46
-13
lines changed

2 files changed

+46
-13
lines changed

src/controllers/krate/versions.rs

Lines changed: 30 additions & 11 deletions
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

@@ -153,7 +157,7 @@ fn list_by_semver(
153157
) -> AppResult<PaginatedVersionsAndPublishers> {
154158
use seek::*;
155159

156-
let (data, total) = if let Some(options) = options {
160+
let (data, total, release_tracks) = if let Some(options) = options {
157161
// Since versions will only increase in the future and both sorting and pagination need to
158162
// happen on the app server, implementing it with fetching only the data needed for sorting
159163
// and pagination, then making another query for the data to respond with, would minimize
@@ -164,18 +168,26 @@ fn list_by_semver(
164168
let mut sorted_versions = IndexMap::new();
165169
for result in versions::table
166170
.filter(versions::crate_id.eq(crate_id))
167-
.select((versions::id, versions::num))
168-
.load_iter::<(i32, String), DefaultLoadingMode>(conn)?
171+
.select((versions::id, versions::num, versions::yanked))
172+
.load_iter::<(i32, String, bool), DefaultLoadingMode>(conn)?
169173
{
170-
let (id, num) = result?;
171-
sorted_versions.insert(id, (num, None));
174+
let (id, num, yanked) = result?;
175+
let semver = semver::Version::parse(&num).ok();
176+
sorted_versions.insert(id, (semver, yanked, None));
172177
}
173-
sorted_versions.sort_by_cached_key(|_, (num, _)| Reverse(semver::Version::parse(num).ok()));
178+
sorted_versions
179+
.sort_unstable_by(|_, (semver_a, _, _), _, (semver_b, _, _)| semver_b.cmp(semver_a));
174180

175181
assert!(
176182
!matches!(&options.page, Page::Numeric(_)),
177183
"?page= is not supported"
178184
);
185+
let release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
186+
sorted_versions
187+
.values()
188+
.filter(|(_, yanked, _)| !yanked)
189+
.filter_map(|(semver, _, _)| semver.as_ref()),
190+
));
179191
let mut idx = Some(0);
180192
if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? {
181193
idx = sorted_versions
@@ -197,19 +209,24 @@ fn list_by_semver(
197209
.load_iter::<(Version, Option<User>), DefaultLoadingMode>(conn)?
198210
{
199211
let row = result?;
200-
sorted_versions.insert(row.0.id, (row.0.num.to_owned(), Some(row)));
212+
// The versions are already sorted, and we only need to enrich the fetched rows into them.
213+
// Therefore, other values can now be safely ignored.
214+
sorted_versions
215+
.entry(row.0.id)
216+
.and_modify(|entry| *entry = (None, false, Some(row)));
201217
}
202218

203219
let len = sorted_versions.len();
204220
(
205221
sorted_versions
206222
.into_values()
207-
.filter_map(|(_, v)| v)
223+
.filter_map(|(_, _, v)| v)
208224
.collect(),
209225
len,
226+
release_tracks,
210227
)
211228
} else {
212-
(vec![], 0)
229+
(vec![], 0, release_tracks)
213230
}
214231
} else {
215232
let mut data: Vec<(Version, Option<User>)> = versions::table
@@ -219,7 +236,7 @@ fn list_by_semver(
219236
.load(conn)?;
220237
data.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok()));
221238
let total = data.len();
222-
(data, total)
239+
(data, total, None)
223240
};
224241

225242
let mut next_page = None;
@@ -233,6 +250,7 @@ fn list_by_semver(
233250
meta: ResponseMeta {
234251
total: total as i64,
235252
next_page,
253+
release_tracks,
236254
},
237255
})
238256
}
@@ -302,6 +320,7 @@ struct PaginatedVersionsAndPublishers {
302320
struct ResponseMeta {
303321
total: i64,
304322
next_page: Option<String>,
323+
release_tracks: Option<ReleaseTracks>,
305324
}
306325

307326
#[derive(Debug, Eq, PartialEq, Serialize)]

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ async fn test_sorting() {
107107
for (json, expect) in resp.iter().zip(expects) {
108108
assert_eq!(json.versions[0].num, expect);
109109
assert_eq!(json.meta.total as usize, expects.len());
110+
assert_eq!(
111+
json.meta.release_tracks,
112+
Some(json!({"1.x": {"highest": "1.0.0"}}))
113+
);
110114
}
111115
assert_eq!(calls as usize, expects.len() + 1);
112116

@@ -131,6 +135,7 @@ async fn test_seek_based_pagination_semver_sorting() {
131135
let user = user.as_model();
132136
app.db(|conn| {
133137
CrateBuilder::new("foo_versions", user.id)
138+
.version(VersionBuilder::new("0.5.2").yanked(true))
134139
.version("0.5.1")
135140
.version(VersionBuilder::new("1.0.0").rust_version("1.64"))
136141
.version("0.5.0")
@@ -146,7 +151,11 @@ async fn test_seek_based_pagination_semver_sorting() {
146151
});
147152

148153
let url = "/api/v1/crates/foo_versions/versions";
149-
let expects = ["1.0.0", "0.5.1", "0.5.0"];
154+
let expects = ["1.0.0", "0.5.2", "0.5.1", "0.5.0"];
155+
let release_tracks = Some(json!({
156+
"1.x": {"highest": "1.0.0"},
157+
"0.5": {"highest": "0.5.1"}
158+
}));
150159

151160
// per_page larger than the number of versions
152161
let json: VersionList = anon
@@ -155,6 +164,7 @@ async fn test_seek_based_pagination_semver_sorting() {
155164
.good();
156165
assert_eq!(nums(&json.versions), expects);
157166
assert_eq!(json.meta.total as usize, expects.len());
167+
assert_eq!(json.meta.release_tracks, release_tracks);
158168

159169
let json: VersionList = anon
160170
.get_with_query(url, "per_page=1&sort=semver")
@@ -178,15 +188,17 @@ async fn test_seek_based_pagination_semver_sorting() {
178188
assert_eq!(nums(&json.versions), expects[1..]);
179189
assert!(json.meta.next_page.is_none());
180190
assert_eq!(json.meta.total as usize, expects.len());
191+
assert_eq!(json.meta.release_tracks, release_tracks);
181192

182193
// per_page euqal to the number of remain versions
183194
let json: VersionList = anon
184195
.get_with_query(url, &format!("per_page=2&sort=semver&seek={seek}"))
185196
.await
186197
.good();
187-
assert_eq!(nums(&json.versions), expects[1..]);
198+
assert_eq!(nums(&json.versions), expects[1..3]);
188199
assert!(json.meta.next_page.is_some());
189200
assert_eq!(json.meta.total as usize, expects.len());
201+
assert_eq!(json.meta.release_tracks, release_tracks);
190202

191203
// A decodable seek value, MTAwCg (100), but doesn't actually exist
192204
let json: VersionList = anon
@@ -196,6 +208,7 @@ async fn test_seek_based_pagination_semver_sorting() {
196208
assert_eq!(json.versions.len(), 0);
197209
assert!(json.meta.next_page.is_none());
198210
assert_eq!(json.meta.total, 0);
211+
assert_eq!(json.meta.release_tracks, release_tracks);
199212
}
200213

201214
#[tokio::test(flavor = "multi_thread")]
@@ -242,6 +255,7 @@ pub struct VersionList {
242255
pub struct ResponseMeta {
243256
pub total: i64,
244257
pub next_page: Option<String>,
258+
pub release_tracks: Option<serde_json::Value>,
245259
}
246260

247261
fn nums(versions: &[EncodableVersion]) -> Vec<String> {

0 commit comments

Comments
 (0)