Skip to content

Commit cdba41c

Browse files
authored
Merge pull request #10238 from Turbo87/query-params-structs
Extract `QueryParams` structs
2 parents 541e1a9 + 04bac90 commit cdba41c

File tree

9 files changed

+359
-58
lines changed

9 files changed

+359
-58
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "json"] }
125125
typomania = { version = "=0.1.2", default-features = false }
126126
url = "=2.5.4"
127127
unicode-xid = "=0.2.6"
128-
utoipa = "=5.2.0"
128+
utoipa = { version = "=5.2.0", features = ["chrono"] }
129129
utoipa-axum = "=0.1.2"
130130

131131
[dev-dependencies]

src/controllers/category.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,47 @@ use crate::app::AppState;
33
use crate::models::Category;
44
use crate::schema::categories;
55
use crate::util::errors::AppResult;
6-
use crate::util::RequestUtils;
76
use crate::views::{EncodableCategory, EncodableCategoryWithSubcategories};
8-
use axum::extract::Path;
7+
use axum::extract::{FromRequestParts, Path, Query};
98
use axum_extra::json;
109
use axum_extra::response::ErasedJson;
1110
use diesel::QueryDsl;
1211
use diesel_async::RunQueryDsl;
1312
use http::request::Parts;
1413

14+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
15+
#[from_request(via(Query))]
16+
#[into_params(parameter_in = Query)]
17+
pub struct ListQueryParams {
18+
/// The sort order of the categories.
19+
///
20+
/// Valid values: `alpha`, and `crates`.
21+
///
22+
/// Defaults to `alpha`.
23+
sort: Option<String>,
24+
}
25+
1526
/// List all categories.
1627
#[utoipa::path(
1728
get,
1829
path = "/api/v1/categories",
30+
params(ListQueryParams, PaginationQueryParams),
1931
tag = "categories",
2032
responses((status = 200, description = "Successful Response")),
2133
)]
22-
pub async fn list_categories(app: AppState, req: Parts) -> AppResult<ErasedJson> {
34+
pub async fn list_categories(
35+
app: AppState,
36+
params: ListQueryParams,
37+
req: Parts,
38+
) -> AppResult<ErasedJson> {
2339
// FIXME: There are 69 categories, 47 top level. This isn't going to
2440
// grow by an OoM. We need a limit for /summary, but we don't need
2541
// to paginate this.
2642
let options = PaginationOptions::builder().gather(&req)?;
2743

2844
let mut conn = app.db_read().await?;
2945

30-
let query = req.query();
31-
let sort = query.get("sort").map_or("alpha", String::as_str);
46+
let sort = params.sort.as_ref().map_or("alpha", String::as_str);
3247

3348
let offset = options.offset().unwrap_or_default();
3449

src/controllers/crate_owner_invitation.rs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
use crate::app::AppState;
22
use crate::auth::AuthCheck;
33
use crate::auth::Authentication;
4-
use crate::controllers::helpers::pagination::{Page, PaginationOptions};
4+
use crate::controllers::helpers::pagination::{Page, PaginationOptions, PaginationQueryParams};
55
use crate::models::{Crate, CrateOwnerInvitation, Rights, User};
66
use crate::schema::{crate_owner_invitations, crates, users};
7-
use crate::util::errors::{bad_request, forbidden, internal, AppResult};
7+
use crate::util::errors::{bad_request, forbidden, internal, AppResult, BoxedAppError};
88
use crate::util::RequestUtils;
99
use crate::views::{
1010
EncodableCrateOwnerInvitation, EncodableCrateOwnerInvitationV1, EncodablePublicUser,
1111
InvitationResponse,
1212
};
13-
use axum::extract::Path;
13+
use axum::extract::{FromRequestParts, Path, Query};
1414
use axum::Json;
1515
use axum_extra::json;
1616
use axum_extra::response::ErasedJson;
@@ -70,28 +70,38 @@ pub async fn list_crate_owner_invitations_for_user(
7070
}))
7171
}
7272

73+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
74+
#[from_request(via(Query))]
75+
#[into_params(parameter_in = Query)]
76+
pub struct ListQueryParams {
77+
/// Filter crate owner invitations by crate name.
78+
///
79+
/// Only crate owners can query pending invitations for their crate.
80+
crate_name: Option<String>,
81+
82+
/// The ID of the user who was invited to be a crate owner.
83+
///
84+
/// This parameter needs to match the authenticated user's ID.
85+
invitee_id: Option<i32>,
86+
}
87+
7388
/// List all crate owner invitations for a crate or user.
7489
#[utoipa::path(
7590
get,
7691
path = "/api/private/crate_owner_invitations",
92+
params(ListQueryParams, PaginationQueryParams),
7793
tag = "owners",
7894
responses((status = 200, description = "Successful Response")),
7995
)]
8096
pub async fn list_crate_owner_invitations(
8197
app: AppState,
98+
params: ListQueryParams,
8299
req: Parts,
83100
) -> AppResult<Json<PrivateListResponse>> {
84101
let mut conn = app.db_read().await?;
85102
let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?;
86103

87-
let filter = if let Some(crate_name) = req.query().get("crate_name") {
88-
ListFilter::CrateName(crate_name.clone())
89-
} else if let Some(id) = req.query().get("invitee_id").and_then(|i| i.parse().ok()) {
90-
ListFilter::InviteeId(id)
91-
} else {
92-
return Err(bad_request("missing or invalid filter"));
93-
};
94-
104+
let filter = params.try_into()?;
95105
let list = prepare_list(&app, &req, auth, filter, &mut conn).await?;
96106
Ok(Json(list))
97107
}
@@ -101,6 +111,22 @@ enum ListFilter {
101111
InviteeId(i32),
102112
}
103113

114+
impl TryFrom<ListQueryParams> for ListFilter {
115+
type Error = BoxedAppError;
116+
117+
fn try_from(params: ListQueryParams) -> Result<Self, Self::Error> {
118+
let filter = if let Some(crate_name) = params.crate_name {
119+
ListFilter::CrateName(crate_name.clone())
120+
} else if let Some(id) = params.invitee_id {
121+
ListFilter::InviteeId(id)
122+
} else {
123+
return Err(bad_request("missing or invalid filter"));
124+
};
125+
126+
Ok(filter)
127+
}
128+
}
129+
104130
async fn prepare_list(
105131
state: &AppState,
106132
req: &Parts,

src/controllers/helpers/pagination.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::util::errors::{bad_request, AppResult};
77
use crate::util::HeaderMapExt;
88
use std::num::NonZeroU32;
99

10+
use axum::extract::FromRequestParts;
1011
use base64::{engine::general_purpose, Engine};
1112
use diesel::pg::Pg;
1213
use diesel::prelude::*;
@@ -55,19 +56,20 @@ impl PaginationOptions {
5556
}
5657
}
5758

58-
#[derive(Debug, Deserialize, utoipa::IntoParams)]
59+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
60+
#[from_request(via(axum::extract::Query))]
5961
#[into_params(parameter_in = Query)]
6062
pub struct PaginationQueryParams {
6163
/// The page number to request.
6264
///
6365
/// This parameter is mutually exclusive with `seek` and not supported for
6466
/// all requests.
6567
#[param(value_type = Option<u32>, minimum = 1)]
66-
page: Option<NonZeroU32>,
68+
pub page: Option<NonZeroU32>,
6769

6870
/// The number of items to request per page.
6971
#[param(value_type = Option<u32>, minimum = 1)]
70-
per_page: Option<NonZeroU32>,
72+
pub per_page: Option<NonZeroU32>,
7173

7274
/// The seek key to request.
7375
///
@@ -76,7 +78,7 @@ pub struct PaginationQueryParams {
7678
///
7779
/// The seek key can usually be found in the `meta.next_page` field of
7880
/// paginated responses.
79-
seek: Option<String>,
81+
pub seek: Option<String>,
8082
}
8183

8284
pub(crate) struct PaginationOptionsBuilder {

src/controllers/krate/metadata.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,30 @@ use crate::models::{
1212
};
1313
use crate::schema::*;
1414
use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError};
15-
use crate::util::RequestUtils;
1615
use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion};
16+
use axum::extract::{FromRequestParts, Query};
1717
use axum_extra::json;
1818
use axum_extra::response::ErasedJson;
1919
use diesel::prelude::*;
2020
use diesel_async::RunQueryDsl;
21-
use http::request::Parts;
2221
use std::cmp::Reverse;
2322
use std::str::FromStr;
2423

24+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
25+
#[from_request(via(Query))]
26+
#[into_params(parameter_in = Query)]
27+
pub struct FindQueryParams {
28+
/// Additional data to include in the response.
29+
///
30+
/// Valid values: `versions`, `keywords`, `categories`, `badges`,
31+
/// `downloads`, or `full`.
32+
///
33+
/// Defaults to `full` for backwards compatibility.
34+
///
35+
/// This parameter expects a comma-separated list of values.
36+
include: Option<String>,
37+
}
38+
2539
/// Get crate metadata (for the `new` crate).
2640
///
2741
/// This endpoint works around a small limitation in `axum` and is delegating
@@ -32,26 +46,29 @@ use std::str::FromStr;
3246
tag = "crates",
3347
responses((status = 200, description = "Successful Response")),
3448
)]
35-
pub async fn find_new_crate(app: AppState, req: Parts) -> AppResult<ErasedJson> {
49+
pub async fn find_new_crate(app: AppState, params: FindQueryParams) -> AppResult<ErasedJson> {
3650
let name = "new".to_string();
37-
find_crate(app, CratePath { name }, req).await
51+
find_crate(app, CratePath { name }, params).await
3852
}
3953

4054
/// Get crate metadata.
4155
#[utoipa::path(
4256
get,
4357
path = "/api/v1/crates/{name}",
44-
params(CratePath),
58+
params(CratePath, FindQueryParams),
4559
tag = "crates",
4660
responses((status = 200, description = "Successful Response")),
4761
)]
48-
pub async fn find_crate(app: AppState, path: CratePath, req: Parts) -> AppResult<ErasedJson> {
62+
pub async fn find_crate(
63+
app: AppState,
64+
path: CratePath,
65+
params: FindQueryParams,
66+
) -> AppResult<ErasedJson> {
4967
let mut conn = app.db_read().await?;
5068

51-
let include = req
52-
.query()
53-
.get("include")
54-
.map(|mode| ShowIncludeMode::from_str(mode))
69+
let include = params
70+
.include
71+
.map(|mode| ShowIncludeMode::from_str(&mode))
5572
.transpose()?
5673
.unwrap_or_default();
5774

src/controllers/krate/versions.rs

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Endpoint for versions of a crate
22
3+
use axum::extract::{FromRequestParts, Query};
34
use axum_extra::json;
45
use axum_extra::response::ErasedJson;
56
use diesel::dsl::not;
@@ -11,48 +12,75 @@ use indexmap::{IndexMap, IndexSet};
1112
use std::str::FromStr;
1213

1314
use crate::app::AppState;
14-
use crate::controllers::helpers::pagination::{encode_seek, Page, PaginationOptions};
15+
use crate::controllers::helpers::pagination::{
16+
encode_seek, Page, PaginationOptions, PaginationQueryParams,
17+
};
1518
use crate::controllers::krate::CratePath;
1619
use crate::models::{User, Version, VersionOwnerAction};
1720
use crate::schema::{users, versions};
1821
use crate::util::errors::{bad_request, AppResult, BoxedAppError};
1922
use crate::util::RequestUtils;
2023
use crate::views::EncodableVersion;
2124

25+
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
26+
#[from_request(via(Query))]
27+
#[into_params(parameter_in = Query)]
28+
pub struct ListQueryParams {
29+
/// Additional data to include in the response.
30+
///
31+
/// Valid values: `release_tracks`.
32+
///
33+
/// Defaults to no additional data.
34+
///
35+
/// This parameter expects a comma-separated list of values.
36+
include: Option<String>,
37+
38+
/// The sort order of the versions.
39+
///
40+
/// Valid values: `date`, and `semver`.
41+
///
42+
/// Defaults to `semver`.
43+
sort: Option<String>,
44+
}
45+
2246
/// List all versions of a crate.
2347
#[utoipa::path(
2448
get,
2549
path = "/api/v1/crates/{name}/versions",
26-
params(CratePath),
50+
params(CratePath, ListQueryParams, PaginationQueryParams),
2751
tag = "versions",
2852
responses((status = 200, description = "Successful Response")),
2953
)]
30-
pub async fn list_versions(state: AppState, path: CratePath, req: Parts) -> AppResult<ErasedJson> {
54+
pub async fn list_versions(
55+
state: AppState,
56+
path: CratePath,
57+
params: ListQueryParams,
58+
pagination: PaginationQueryParams,
59+
req: Parts,
60+
) -> AppResult<ErasedJson> {
3161
let mut conn = state.db_read().await?;
3262

3363
let crate_id = path.load_crate_id(&mut conn).await?;
3464

35-
let mut pagination = None;
36-
let params = req.query();
3765
// To keep backward compatibility, we paginate only if per_page is provided
38-
if params.get("per_page").is_some() {
39-
pagination = Some(
66+
let pagination = match pagination.per_page {
67+
Some(_) => Some(
4068
PaginationOptions::builder()
4169
.enable_seek(true)
4270
.enable_pages(false)
4371
.gather(&req)?,
44-
);
45-
}
72+
),
73+
None => None,
74+
};
4675

47-
let include = req
48-
.query()
49-
.get("include")
50-
.map(|mode| ShowIncludeMode::from_str(mode))
76+
let include = params
77+
.include
78+
.map(|mode| ShowIncludeMode::from_str(&mode))
5179
.transpose()?
5280
.unwrap_or_default();
5381

5482
// Sort by semver by default
55-
let versions_and_publishers = match params.get("sort").map(|s| s.to_lowercase()).as_deref() {
83+
let versions_and_publishers = match params.sort.map(|s| s.to_lowercase()).as_deref() {
5684
Some("date") => {
5785
list_by_date(crate_id, pagination.as_ref(), include, &req, &mut conn).await?
5886
}

0 commit comments

Comments
 (0)