Skip to content

Commit 97481be

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 e170895 commit 97481be

File tree

2 files changed

+111
-55
lines changed

2 files changed

+111
-55
lines changed

src/controllers/krate/versions.rs

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ use http::request::Parts;
1010
use indexmap::{IndexMap, IndexSet};
1111
use serde_json::Value;
1212
use std::cmp::Reverse;
13+
use std::str::FromStr;
1314

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

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

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

5967
let versions = versions_and_publishers
@@ -85,6 +93,7 @@ pub async fn versions(
8593
fn list_by_date(
8694
crate_id: i32,
8795
options: Option<&PaginationOptions>,
96+
include: ShowIncludeMode,
8897
req: &Parts,
8998
conn: &mut impl Conn,
9099
) -> AppResult<PaginatedVersionsAndPublishers> {
@@ -112,22 +121,24 @@ fn list_by_date(
112121
}
113122
query = query.limit(options.per_page);
114123

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

133144
query = query.order((versions::created_at.desc(), versions::id.desc()));
@@ -171,6 +182,7 @@ fn list_by_date(
171182
fn list_by_semver(
172183
crate_id: i32,
173184
options: Option<&PaginationOptions>,
185+
include: ShowIncludeMode,
174186
req: &Parts,
175187
conn: &mut impl Conn,
176188
) -> AppResult<PaginatedVersionsAndPublishers> {
@@ -201,12 +213,16 @@ fn list_by_semver(
201213
!matches!(&options.page, Page::Numeric(_)),
202214
"?page= is not supported"
203215
);
204-
let release_tracks = Some(ReleaseTracks::from_sorted_semver_iter(
205-
sorted_versions
206-
.values()
207-
.filter(|(_, yanked, _)| !yanked)
208-
.filter_map(|(semver, _, _)| semver.as_ref()),
209-
));
216+
217+
let release_tracks = include.release_tracks.then(|| {
218+
ReleaseTracks::from_sorted_semver_iter(
219+
sorted_versions
220+
.values()
221+
.filter(|(_, yanked, _)| !yanked)
222+
.filter_map(|(semver, _, _)| semver.as_ref()),
223+
)
224+
});
225+
210226
let mut idx = Some(0);
211227
if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? {
212228
idx = sorted_versions
@@ -411,6 +427,34 @@ impl serde::Serialize for ReleaseSlug {
411427
}
412428
}
413429

430+
#[derive(Debug, Default)]
431+
struct ShowIncludeMode {
432+
release_tracks: bool,
433+
}
434+
435+
impl ShowIncludeMode {
436+
const INVALID_COMPONENT: &'static str =
437+
"invalid component for ?include= (expected 'release_tracks')";
438+
}
439+
440+
impl FromStr for ShowIncludeMode {
441+
type Err = BoxedAppError;
442+
443+
fn from_str(s: &str) -> Result<Self, Self::Err> {
444+
let mut mode = Self {
445+
release_tracks: false,
446+
};
447+
for component in s.split(',') {
448+
match component {
449+
"" => {}
450+
"release_tracks" => mode.release_tracks = true,
451+
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
452+
}
453+
}
454+
Ok(mode)
455+
}
456+
}
457+
414458
#[cfg(test)]
415459
mod tests {
416460
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": {"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": {"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": {"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)