Skip to content

Commit ad9c8e0

Browse files
committed
controllers/krate/versions: Add ShowIncludeMode for pagination
This enables clients to specify the `include` query param to include `release_tracks` meta.
1 parent c8883e5 commit ad9c8e0

File tree

2 files changed

+111
-55
lines changed

2 files changed

+111
-55
lines changed

src/controllers/krate/versions.rs

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ use http::request::Parts;
99
use indexmap::{IndexMap, IndexSet};
1010
use serde_json::Value;
1111
use std::cmp::Reverse;
12+
use std::str::FromStr;
1213

1314
use crate::app::AppState;
1415
use crate::controllers::helpers::pagination::{encode_seek, Page, PaginationOptions};
1516
use crate::models::{Crate, User, Version, VersionOwnerAction};
1617
use crate::schema::{crates, users, versions};
1718
use crate::tasks::spawn_blocking;
1819
use crate::util::diesel::Conn;
19-
use crate::util::errors::{crate_not_found, AppResult};
20+
use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError};
2021
use crate::util::RequestUtils;
2122
use crate::views::EncodableVersion;
2223

@@ -48,11 +49,18 @@ pub async fn versions(
4849
);
4950
}
5051

52+
let include = req
53+
.query()
54+
.get("include")
55+
.map(|mode| ShowIncludeMode::from_str(mode))
56+
.transpose()?
57+
.unwrap_or_default();
58+
5159
// Sort by semver by default
5260
let versions_and_publishers = match params.get("sort").map(|s| s.to_lowercase()).as_deref()
5361
{
54-
Some("date") => list_by_date(crate_id, pagination.as_ref(), &req, conn)?,
55-
_ => list_by_semver(crate_id, pagination.as_ref(), &req, conn)?,
62+
Some("date") => list_by_date(crate_id, pagination.as_ref(), include, &req, conn)?,
63+
_ => list_by_semver(crate_id, pagination.as_ref(), include, &req, conn)?,
5664
};
5765

5866
let versions = versions_and_publishers
@@ -84,6 +92,7 @@ pub async fn versions(
8492
fn list_by_date(
8593
crate_id: i32,
8694
options: Option<&PaginationOptions>,
95+
include: ShowIncludeMode,
8796
req: &Parts,
8897
conn: &mut impl Conn,
8998
) -> AppResult<PaginatedVersionsAndPublishers> {
@@ -111,22 +120,24 @@ fn list_by_date(
111120
}
112121
query = query.limit(options.per_page);
113122

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);
123+
if include.release_tracks {
124+
let mut sorted_versions = IndexSet::new();
125+
for result in versions::table
126+
.filter(versions::crate_id.eq(crate_id))
127+
.filter(versions::yanked.ne(true))
128+
.select(versions::num)
129+
.load_iter::<String, DefaultLoadingMode>(conn)?
130+
{
131+
let Ok(semver) = semver::Version::parse(&result?) else {
132+
continue;
133+
};
134+
sorted_versions.insert(semver);
135+
}
136+
sorted_versions.sort_unstable_by(|a, b| b.cmp(a));
137+
release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
138+
sorted_versions.iter(),
139+
));
125140
}
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-
));
130141
}
131142

132143
query = query.order((versions::created_at.desc(), versions::id.desc()));
@@ -170,6 +181,7 @@ fn list_by_date(
170181
fn list_by_semver(
171182
crate_id: i32,
172183
options: Option<&PaginationOptions>,
184+
include: ShowIncludeMode,
173185
req: &Parts,
174186
conn: &mut impl Conn,
175187
) -> AppResult<PaginatedVersionsAndPublishers> {
@@ -200,12 +212,16 @@ fn list_by_semver(
200212
!matches!(&options.page, Page::Numeric(_)),
201213
"?page= is not supported"
202214
);
203-
let release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
204-
sorted_versions
205-
.values()
206-
.filter(|(_, yanked, _)| !yanked)
207-
.filter_map(|(semver, _, _)| semver.as_ref()),
208-
));
215+
216+
let release_tracks = include.release_tracks.then(|| {
217+
ReleaseTracks::from_sorted_semver_iter(
218+
sorted_versions
219+
.values()
220+
.filter(|(_, yanked, _)| !yanked)
221+
.filter_map(|(semver, _, _)| semver.as_ref()),
222+
)
223+
});
224+
209225
let mut idx = Some(0);
210226
if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? {
211227
idx = sorted_versions
@@ -435,6 +451,39 @@ impl serde::Serialize for SemverTriple {
435451
}
436452
}
437453

454+
#[derive(Debug, Default)]
455+
struct ShowIncludeMode {
456+
release_tracks: bool,
457+
}
458+
459+
impl ShowIncludeMode {
460+
const INVALID_COMPONENT: &'static str =
461+
"invalid component for ?include= (expected 'release_tracks', or 'full')";
462+
}
463+
464+
impl FromStr for ShowIncludeMode {
465+
type Err = BoxedAppError;
466+
467+
fn from_str(s: &str) -> Result<Self, Self::Err> {
468+
let mut mode = Self {
469+
release_tracks: false,
470+
};
471+
for component in s.split(',') {
472+
match component {
473+
"" => {}
474+
"full" => {
475+
mode = Self {
476+
release_tracks: true,
477+
}
478+
}
479+
"release_tracks" => mode.release_tracks = true,
480+
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
481+
}
482+
}
483+
Ok(mode)
484+
}
485+
}
486+
438487
#[cfg(test)]
439488
mod tests {
440489
use super::{HighestSemver, ReleaseSlug, ReleaseTracks};

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

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@ async fn test_sorting() {
8686
.expect_build(conn);
8787
});
8888

89-
// Sort by semver
90-
let url = "/api/v1/crates/foo_versions/versions?sort=semver";
91-
let json: AllVersions = anon.get(url).await.good();
9289
let expects = [
9390
"2.0.0-alpha",
9491
"1.0.0",
@@ -100,19 +97,26 @@ async fn test_sorting() {
10097
"1.0.0-alpha.1",
10198
"1.0.0-alpha",
10299
];
100+
let release_tracks = Some(json!({"1.x": {"highest": "1.0.0"}}));
101+
102+
// Sort by semver
103+
let url = "/api/v1/crates/foo_versions/versions?sort=semver";
104+
let json: AllVersions = anon.get(url).await.good();
103105
for (num, expect) in nums(&json.versions).iter().zip(expects) {
104106
assert_eq!(num, expect);
105107
}
106-
let (resp, calls) = page_with_seek(&anon, url).await;
107-
for (json, expect) in resp.iter().zip(expects) {
108-
assert_eq!(json.versions[0].num, expect);
109-
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-
);
108+
for (url, release_tracks) in [
109+
(url, None),
110+
(&format!("{url}&include=release_tracks"), release_tracks.as_ref()),
111+
] {
112+
let (resp, calls) = page_with_seek(&anon, url).await;
113+
for (json, expect) in resp.iter().zip(expects) {
114+
assert_eq!(json.versions[0].num, expect);
115+
assert_eq!(json.meta.total as usize, expects.len());
116+
assert_eq!(json.meta.release_tracks.as_ref(), release_tracks);
117+
}
118+
assert_eq!(calls as usize, expects.len() + 1);
114119
}
115-
assert_eq!(calls as usize, expects.len() + 1);
116120

117121
// Sort by date
118122
let url = "/api/v1/crates/foo_versions/versions?sort=date";
@@ -121,16 +125,19 @@ async fn test_sorting() {
121125
for (num, expect) in nums(&json.versions).iter().zip(&expects) {
122126
assert_eq!(num, *expect);
123127
}
124-
let (resp, calls) = page_with_seek(&anon, url).await;
125-
for (json, expect) in resp.iter().zip(&expects) {
126-
assert_eq!(json.versions[0].num, *expect);
127-
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-
);
128+
for (url, release_tracks) in [
129+
(url, None),
130+
(&format!("{url}&include=release_tracks"), release_tracks.as_ref()),
131+
] {
132+
let (resp, calls) = page_with_seek(&anon, url).await;
133+
for (json, expect) in resp.iter().zip(&expects) {
134+
assert_eq!(json.versions[0].num, *expect);
135+
assert_eq!(json.meta.total as usize, expects.len());
136+
assert_eq!(json.meta.release_tracks.as_ref(), release_tracks);
137+
}
138+
assert_eq!(calls as usize, expects.len() + 1);
132139
}
133-
assert_eq!(calls as usize, expects.len() + 1);
140+
134141
}
135142

136143
#[tokio::test(flavor = "multi_thread")]
@@ -167,10 +174,10 @@ async fn test_seek_based_pagination_semver_sorting() {
167174
.good();
168175
assert_eq!(nums(&json.versions), expects);
169176
assert_eq!(json.meta.total as usize, expects.len());
170-
assert_eq!(json.meta.release_tracks, release_tracks);
177+
assert_eq!(json.meta.release_tracks, None);
171178

172179
let json: VersionList = anon
173-
.get_with_query(url, "per_page=1&sort=semver")
180+
.get_with_query(url, "per_page=1&sort=semver&include=release_tracks")
174181
.await
175182
.good();
176183
assert_eq!(nums(&json.versions), expects[0..1]);
@@ -192,7 +199,7 @@ async fn test_seek_based_pagination_semver_sorting() {
192199
assert_eq!(nums(&json.versions), expects[1..]);
193200
assert!(json.meta.next_page.is_none());
194201
assert_eq!(json.meta.total as usize, expects.len());
195-
assert_eq!(json.meta.release_tracks, release_tracks);
202+
assert_eq!(json.meta.release_tracks, None);
196203

197204
// per_page euqal to the number of remain versions
198205
let json: VersionList = anon
@@ -202,7 +209,7 @@ async fn test_seek_based_pagination_semver_sorting() {
202209
assert_eq!(nums(&json.versions), expects[1..]);
203210
assert!(json.meta.next_page.is_some());
204211
assert_eq!(json.meta.total as usize, expects.len());
205-
assert_eq!(json.meta.release_tracks, release_tracks);
212+
assert_eq!(json.meta.release_tracks, None);
206213

207214
// A decodable seek value, MTAwCg (100), but doesn't actually exist
208215
let json: VersionList = anon
@@ -212,7 +219,7 @@ async fn test_seek_based_pagination_semver_sorting() {
212219
assert_eq!(json.versions.len(), 0);
213220
assert!(json.meta.next_page.is_none());
214221
assert_eq!(json.meta.total, 0);
215-
assert_eq!(json.meta.release_tracks, release_tracks);
222+
assert_eq!(json.meta.release_tracks, None);
216223
}
217224

218225
#[tokio::test(flavor = "multi_thread")]
@@ -249,10 +256,10 @@ async fn test_seek_based_pagination_date_sorting() {
249256
.good();
250257
assert_eq!(nums(&json.versions), expects);
251258
assert_eq!(json.meta.total as usize, expects.len());
252-
assert_eq!(json.meta.release_tracks, release_tracks);
259+
assert_eq!(json.meta.release_tracks, None);
253260

254261
let json: VersionList = anon
255-
.get_with_query(url, "per_page=1&sort=date")
262+
.get_with_query(url, "per_page=1&sort=date&include=release_tracks")
256263
.await
257264
.good();
258265
assert_eq!(nums(&json.versions), expects[0..1]);
@@ -274,7 +281,7 @@ async fn test_seek_based_pagination_date_sorting() {
274281
assert_eq!(nums(&json.versions), expects[1..]);
275282
assert!(json.meta.next_page.is_none());
276283
assert_eq!(json.meta.total as usize, expects.len());
277-
assert_eq!(json.meta.release_tracks, release_tracks);
284+
assert_eq!(json.meta.release_tracks, None);
278285

279286
// per_page euqal to the number of remain versions
280287
let json: VersionList = anon
@@ -284,7 +291,7 @@ async fn test_seek_based_pagination_date_sorting() {
284291
assert_eq!(nums(&json.versions), expects[1..]);
285292
assert!(json.meta.next_page.is_some());
286293
assert_eq!(json.meta.total as usize, expects.len());
287-
assert_eq!(json.meta.release_tracks, release_tracks);
294+
assert_eq!(json.meta.release_tracks, None);
288295

289296
// A decodable seek value, WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K ([1728561992734673,5]), but doesn't actually exist
290297
let json: VersionList = anon
@@ -297,7 +304,7 @@ async fn test_seek_based_pagination_date_sorting() {
297304
assert_eq!(json.versions.len(), 0);
298305
assert!(json.meta.next_page.is_none());
299306
assert_eq!(json.meta.total, 0);
300-
assert_eq!(json.meta.release_tracks, release_tracks);
307+
assert_eq!(json.meta.release_tracks, None);
301308
}
302309

303310
#[tokio::test(flavor = "multi_thread")]

0 commit comments

Comments
 (0)