Skip to content

Commit 04b22b6

Browse files
committed
controllers/krate/versions: Add multiple ids[] parameters support for GET /api/v1/crates/:id/versions
1 parent 02a1437 commit 04b22b6

File tree

3 files changed

+111
-15
lines changed

3 files changed

+111
-15
lines changed

src/controllers/krate/versions.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Endpoint for versions of a crate
22
3-
use axum::extract::{FromRequestParts, Query};
3+
use axum::extract::FromRequestParts;
4+
use axum_extra::extract::Query;
45
use axum_extra::json;
56
use axum_extra::response::ErasedJson;
67
use diesel::dsl::not;
@@ -19,6 +20,7 @@ use crate::controllers::krate::CratePath;
1920
use crate::models::{User, Version, VersionOwnerAction};
2021
use crate::schema::{users, versions};
2122
use crate::util::errors::{bad_request, AppResult, BoxedAppError};
23+
use crate::util::string_excl_null::StringExclNull;
2224
use crate::util::RequestUtils;
2325
use crate::views::EncodableVersion;
2426

@@ -41,6 +43,11 @@ pub struct ListQueryParams {
4143
///
4244
/// Defaults to `semver`.
4345
sort: Option<String>,
46+
47+
/// If set, only versions with the specified semver strings are returned.
48+
#[serde(rename = "ids[]", default)]
49+
#[param(inline)]
50+
ids: Vec<StringExclNull>,
4451
}
4552

4653
impl ListQueryParams {
@@ -123,11 +130,20 @@ async fn list_by_date(
123130
) -> AppResult<PaginatedVersionsAndPublishers> {
124131
use seek::*;
125132

126-
let mut query = versions::table
127-
.filter(versions::crate_id.eq(crate_id))
128-
.left_outer_join(users::table)
129-
.select(<(Version, Option<User>)>::as_select())
130-
.into_boxed();
133+
let make_base_query = || {
134+
let mut query = versions::table
135+
.filter(versions::crate_id.eq(crate_id))
136+
.left_outer_join(users::table)
137+
.select(<(Version, Option<User>)>::as_select())
138+
.into_boxed();
139+
140+
if !params.ids.is_empty() {
141+
query = query.filter(versions::num.eq_any(params.ids.iter().map(|s| s.as_str())));
142+
}
143+
query
144+
};
145+
146+
let mut query = make_base_query();
131147

132148
if let Some(options) = options {
133149
assert!(
@@ -192,11 +208,7 @@ async fn list_by_date(
192208
// Since the total count is retrieved through an additional query, to maintain consistency
193209
// with other pagination methods, we only make a count query while data is not empty.
194210
let total = if !data.is_empty() {
195-
versions::table
196-
.filter(versions::crate_id.eq(crate_id))
197-
.count()
198-
.get_result(conn)
199-
.await?
211+
make_base_query().count().get_result(conn).await?
200212
} else {
201213
0
202214
};
@@ -229,6 +241,14 @@ async fn list_by_semver(
229241
use seek::*;
230242

231243
let include = params.include()?;
244+
let mut query = versions::table
245+
.filter(versions::crate_id.eq(crate_id))
246+
.into_boxed();
247+
248+
if !params.ids.is_empty() {
249+
query = query.filter(versions::num.eq_any(params.ids.iter().map(|s| s.as_str())));
250+
}
251+
232252
let (data, total, release_tracks) = if let Some(options) = options {
233253
// Since versions will only increase in the future and both sorting and pagination need to
234254
// happen on the app server, implementing it with fetching only the data needed for sorting
@@ -239,8 +259,7 @@ async fn list_by_semver(
239259
// while id values are significantly smaller.
240260

241261
let mut sorted_versions = IndexMap::new();
242-
versions::table
243-
.filter(versions::crate_id.eq(crate_id))
262+
query
244263
.select((versions::id, versions::num, versions::yanked))
245264
.load_stream::<(i32, String, bool)>(conn)
246265
.await?
@@ -313,8 +332,7 @@ async fn list_by_semver(
313332
}
314333
} else {
315334
let mut data = IndexMap::new();
316-
versions::table
317-
.filter(versions::crate_id.eq(crate_id))
335+
query
318336
.left_outer_join(users::table)
319337
.select(<(Version, Option<User>)>::as_select())
320338
.load_stream::<(Version, Option<User>)>(conn)

src/snapshots/crates_io__openapi__tests__openapi_snapshot.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,19 @@ snapshot_kind: text
856856
"type": "string"
857857
}
858858
},
859+
{
860+
"description": "If set, only versions with the specified semver strings are returned.",
861+
"in": "query",
862+
"name": "ids[]",
863+
"required": false,
864+
"schema": {
865+
"items": {
866+
"description": "A string that does not contain null bytes (`\\0`).",
867+
"type": "string"
868+
},
869+
"type": "array"
870+
}
871+
},
859872
{
860873
"description": "The page number to request.\n\nThis parameter is mutually exclusive with `seek` and not supported for\nall requests.",
861874
"in": "query",

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,71 @@ async fn test_sorting() -> anyhow::Result<()> {
153153
Ok(())
154154
}
155155

156+
#[tokio::test(flavor = "multi_thread")]
157+
async fn multiple_ids() -> anyhow::Result<()> {
158+
let (app, anon, user) = TestApp::init().with_user().await;
159+
let mut conn = app.db_conn().await;
160+
let user = user.as_model();
161+
let mut builder = CrateBuilder::new("foo_versions", user.id);
162+
163+
let versions = [
164+
"2.0.0",
165+
"2.0.0-alpha",
166+
"1.0.0-alpha.beta",
167+
"1.0.0-beta.11",
168+
"1.0.0-beta",
169+
"1.0.0",
170+
"0.5.1",
171+
"0.5.0",
172+
];
173+
for version in versions {
174+
builder = builder.version(version);
175+
}
176+
builder.expect_build(&mut conn).await;
177+
178+
// Sort by semver without pagination
179+
let url = "/api/v1/crates/foo_versions/versions";
180+
let query = [
181+
"ids[]=0.5.1",
182+
"ids[]=1.0.0-alpha.beta",
183+
"ids[]=1.0.0-beta",
184+
"ids[]=2.0.0",
185+
"ids[]=unknown",
186+
]
187+
.join("&");
188+
let json: VersionList = anon.get_with_query(url, &query).await.good();
189+
let expects = ["2.0.0", "1.0.0-beta", "1.0.0-alpha.beta", "0.5.1"];
190+
assert_eq!(nums(&json.versions), expects);
191+
assert!(json.meta.next_page.is_none());
192+
assert_eq!(json.meta.total as usize, expects.len());
193+
assert_eq!(json.meta.release_tracks, None);
194+
195+
let (resp, calls) = page_with_seek(&anon, &format!("{url}?{query}")).await;
196+
for (json, expect) in resp.iter().zip(expects) {
197+
assert_eq!(json.versions[0].num, expect);
198+
assert_eq!(json.meta.total as usize, expects.len());
199+
}
200+
assert_eq!(calls as usize, expects.len() + 1);
201+
202+
// Sort by date without pagination
203+
let query = format!("{query}&sort=date");
204+
let json: VersionList = anon.get_with_query(url, &query).await.good();
205+
let expects = ["0.5.1", "1.0.0-beta", "1.0.0-alpha.beta", "2.0.0"];
206+
assert_eq!(nums(&json.versions), expects);
207+
assert!(json.meta.next_page.is_none());
208+
assert_eq!(json.meta.total as usize, expects.len());
209+
assert_eq!(json.meta.release_tracks, None);
210+
211+
let (resp, calls) = page_with_seek(&anon, &format!("{url}?{query}")).await;
212+
for (json, expect) in resp.iter().zip(expects) {
213+
assert_eq!(json.versions[0].num, expect);
214+
assert_eq!(json.meta.total as usize, expects.len());
215+
}
216+
assert_eq!(calls as usize, expects.len() + 1);
217+
218+
Ok(())
219+
}
220+
156221
#[tokio::test(flavor = "multi_thread")]
157222
async fn test_seek_based_pagination_semver_sorting() -> anyhow::Result<()> {
158223
let (app, anon, user) = TestApp::init().with_user().await;

0 commit comments

Comments
 (0)