Skip to content

Commit 8fa191f

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 8fa191f

File tree

2 files changed

+116
-55
lines changed

2 files changed

+116
-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: 43 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,29 @@ 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+
(
111+
&format!("{url}&include=release_tracks"),
112+
release_tracks.as_ref(),
113+
),
114+
] {
115+
let (resp, calls) = page_with_seek(&anon, url).await;
116+
for (json, expect) in resp.iter().zip(expects) {
117+
assert_eq!(json.versions[0].num, expect);
118+
assert_eq!(json.meta.total as usize, expects.len());
119+
assert_eq!(json.meta.release_tracks.as_ref(), release_tracks);
120+
}
121+
assert_eq!(calls as usize, expects.len() + 1);
114122
}
115-
assert_eq!(calls as usize, expects.len() + 1);
116123

117124
// Sort by date
118125
let url = "/api/v1/crates/foo_versions/versions?sort=date";
@@ -121,16 +128,21 @@ async fn test_sorting() {
121128
for (num, expect) in nums(&json.versions).iter().zip(&expects) {
122129
assert_eq!(num, *expect);
123130
}
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-
);
131+
for (url, release_tracks) in [
132+
(url, None),
133+
(
134+
&format!("{url}&include=release_tracks"),
135+
release_tracks.as_ref(),
136+
),
137+
] {
138+
let (resp, calls) = page_with_seek(&anon, url).await;
139+
for (json, expect) in resp.iter().zip(&expects) {
140+
assert_eq!(json.versions[0].num, *expect);
141+
assert_eq!(json.meta.total as usize, expects.len());
142+
assert_eq!(json.meta.release_tracks.as_ref(), release_tracks);
143+
}
144+
assert_eq!(calls as usize, expects.len() + 1);
132145
}
133-
assert_eq!(calls as usize, expects.len() + 1);
134146
}
135147

136148
#[tokio::test(flavor = "multi_thread")]
@@ -167,10 +179,10 @@ async fn test_seek_based_pagination_semver_sorting() {
167179
.good();
168180
assert_eq!(nums(&json.versions), expects);
169181
assert_eq!(json.meta.total as usize, expects.len());
170-
assert_eq!(json.meta.release_tracks, release_tracks);
182+
assert_eq!(json.meta.release_tracks, None);
171183

172184
let json: VersionList = anon
173-
.get_with_query(url, "per_page=1&sort=semver")
185+
.get_with_query(url, "per_page=1&sort=semver&include=release_tracks")
174186
.await
175187
.good();
176188
assert_eq!(nums(&json.versions), expects[0..1]);
@@ -192,7 +204,7 @@ async fn test_seek_based_pagination_semver_sorting() {
192204
assert_eq!(nums(&json.versions), expects[1..]);
193205
assert!(json.meta.next_page.is_none());
194206
assert_eq!(json.meta.total as usize, expects.len());
195-
assert_eq!(json.meta.release_tracks, release_tracks);
207+
assert_eq!(json.meta.release_tracks, None);
196208

197209
// per_page euqal to the number of remain versions
198210
let json: VersionList = anon
@@ -202,7 +214,7 @@ async fn test_seek_based_pagination_semver_sorting() {
202214
assert_eq!(nums(&json.versions), expects[1..]);
203215
assert!(json.meta.next_page.is_some());
204216
assert_eq!(json.meta.total as usize, expects.len());
205-
assert_eq!(json.meta.release_tracks, release_tracks);
217+
assert_eq!(json.meta.release_tracks, None);
206218

207219
// A decodable seek value, MTAwCg (100), but doesn't actually exist
208220
let json: VersionList = anon
@@ -212,7 +224,7 @@ async fn test_seek_based_pagination_semver_sorting() {
212224
assert_eq!(json.versions.len(), 0);
213225
assert!(json.meta.next_page.is_none());
214226
assert_eq!(json.meta.total, 0);
215-
assert_eq!(json.meta.release_tracks, release_tracks);
227+
assert_eq!(json.meta.release_tracks, None);
216228
}
217229

218230
#[tokio::test(flavor = "multi_thread")]
@@ -249,10 +261,10 @@ async fn test_seek_based_pagination_date_sorting() {
249261
.good();
250262
assert_eq!(nums(&json.versions), expects);
251263
assert_eq!(json.meta.total as usize, expects.len());
252-
assert_eq!(json.meta.release_tracks, release_tracks);
264+
assert_eq!(json.meta.release_tracks, None);
253265

254266
let json: VersionList = anon
255-
.get_with_query(url, "per_page=1&sort=date")
267+
.get_with_query(url, "per_page=1&sort=date&include=release_tracks")
256268
.await
257269
.good();
258270
assert_eq!(nums(&json.versions), expects[0..1]);
@@ -274,7 +286,7 @@ async fn test_seek_based_pagination_date_sorting() {
274286
assert_eq!(nums(&json.versions), expects[1..]);
275287
assert!(json.meta.next_page.is_none());
276288
assert_eq!(json.meta.total as usize, expects.len());
277-
assert_eq!(json.meta.release_tracks, release_tracks);
289+
assert_eq!(json.meta.release_tracks, None);
278290

279291
// per_page euqal to the number of remain versions
280292
let json: VersionList = anon
@@ -284,7 +296,7 @@ async fn test_seek_based_pagination_date_sorting() {
284296
assert_eq!(nums(&json.versions), expects[1..]);
285297
assert!(json.meta.next_page.is_some());
286298
assert_eq!(json.meta.total as usize, expects.len());
287-
assert_eq!(json.meta.release_tracks, release_tracks);
299+
assert_eq!(json.meta.release_tracks, None);
288300

289301
// A decodable seek value, WzE3Mjg1NjE5OTI3MzQ2NzMsNV0K ([1728561992734673,5]), but doesn't actually exist
290302
let json: VersionList = anon
@@ -297,7 +309,7 @@ async fn test_seek_based_pagination_date_sorting() {
297309
assert_eq!(json.versions.len(), 0);
298310
assert!(json.meta.next_page.is_none());
299311
assert_eq!(json.meta.total, 0);
300-
assert_eq!(json.meta.release_tracks, release_tracks);
312+
assert_eq!(json.meta.release_tracks, None);
301313
}
302314

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

0 commit comments

Comments
 (0)