Skip to content

Commit

Permalink
Merge pull request #10238 from Turbo87/query-params-structs
Browse files Browse the repository at this point in the history
Extract `QueryParams` structs
  • Loading branch information
Turbo87 authored Dec 18, 2024
2 parents 541e1a9 + 04bac90 commit cdba41c
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 58 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "json"] }
typomania = { version = "=0.1.2", default-features = false }
url = "=2.5.4"
unicode-xid = "=0.2.6"
utoipa = "=5.2.0"
utoipa = { version = "=5.2.0", features = ["chrono"] }
utoipa-axum = "=0.1.2"

[dev-dependencies]
Expand Down
25 changes: 20 additions & 5 deletions src/controllers/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,47 @@ use crate::app::AppState;
use crate::models::Category;
use crate::schema::categories;
use crate::util::errors::AppResult;
use crate::util::RequestUtils;
use crate::views::{EncodableCategory, EncodableCategoryWithSubcategories};
use axum::extract::Path;
use axum::extract::{FromRequestParts, Path, Query};
use axum_extra::json;
use axum_extra::response::ErasedJson;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use http::request::Parts;

#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
#[from_request(via(Query))]
#[into_params(parameter_in = Query)]
pub struct ListQueryParams {
/// The sort order of the categories.
///
/// Valid values: `alpha`, and `crates`.
///
/// Defaults to `alpha`.
sort: Option<String>,
}

/// List all categories.
#[utoipa::path(
get,
path = "/api/v1/categories",
params(ListQueryParams, PaginationQueryParams),
tag = "categories",
responses((status = 200, description = "Successful Response")),
)]
pub async fn list_categories(app: AppState, req: Parts) -> AppResult<ErasedJson> {
pub async fn list_categories(
app: AppState,
params: ListQueryParams,
req: Parts,
) -> AppResult<ErasedJson> {
// FIXME: There are 69 categories, 47 top level. This isn't going to
// grow by an OoM. We need a limit for /summary, but we don't need
// to paginate this.
let options = PaginationOptions::builder().gather(&req)?;

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

let query = req.query();
let sort = query.get("sort").map_or("alpha", String::as_str);
let sort = params.sort.as_ref().map_or("alpha", String::as_str);

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

Expand Down
48 changes: 37 additions & 11 deletions src/controllers/crate_owner_invitation.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use crate::app::AppState;
use crate::auth::AuthCheck;
use crate::auth::Authentication;
use crate::controllers::helpers::pagination::{Page, PaginationOptions};
use crate::controllers::helpers::pagination::{Page, PaginationOptions, PaginationQueryParams};
use crate::models::{Crate, CrateOwnerInvitation, Rights, User};
use crate::schema::{crate_owner_invitations, crates, users};
use crate::util::errors::{bad_request, forbidden, internal, AppResult};
use crate::util::errors::{bad_request, forbidden, internal, AppResult, BoxedAppError};
use crate::util::RequestUtils;
use crate::views::{
EncodableCrateOwnerInvitation, EncodableCrateOwnerInvitationV1, EncodablePublicUser,
InvitationResponse,
};
use axum::extract::Path;
use axum::extract::{FromRequestParts, Path, Query};
use axum::Json;
use axum_extra::json;
use axum_extra::response::ErasedJson;
Expand Down Expand Up @@ -70,28 +70,38 @@ pub async fn list_crate_owner_invitations_for_user(
}))
}

#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
#[from_request(via(Query))]
#[into_params(parameter_in = Query)]
pub struct ListQueryParams {
/// Filter crate owner invitations by crate name.
///
/// Only crate owners can query pending invitations for their crate.
crate_name: Option<String>,

/// The ID of the user who was invited to be a crate owner.
///
/// This parameter needs to match the authenticated user's ID.
invitee_id: Option<i32>,
}

/// List all crate owner invitations for a crate or user.
#[utoipa::path(
get,
path = "/api/private/crate_owner_invitations",
params(ListQueryParams, PaginationQueryParams),
tag = "owners",
responses((status = 200, description = "Successful Response")),
)]
pub async fn list_crate_owner_invitations(
app: AppState,
params: ListQueryParams,
req: Parts,
) -> AppResult<Json<PrivateListResponse>> {
let mut conn = app.db_read().await?;
let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?;

let filter = if let Some(crate_name) = req.query().get("crate_name") {
ListFilter::CrateName(crate_name.clone())
} else if let Some(id) = req.query().get("invitee_id").and_then(|i| i.parse().ok()) {
ListFilter::InviteeId(id)
} else {
return Err(bad_request("missing or invalid filter"));
};

let filter = params.try_into()?;
let list = prepare_list(&app, &req, auth, filter, &mut conn).await?;
Ok(Json(list))
}
Expand All @@ -101,6 +111,22 @@ enum ListFilter {
InviteeId(i32),
}

impl TryFrom<ListQueryParams> for ListFilter {
type Error = BoxedAppError;

fn try_from(params: ListQueryParams) -> Result<Self, Self::Error> {
let filter = if let Some(crate_name) = params.crate_name {
ListFilter::CrateName(crate_name.clone())
} else if let Some(id) = params.invitee_id {
ListFilter::InviteeId(id)
} else {
return Err(bad_request("missing or invalid filter"));
};

Ok(filter)
}
}

async fn prepare_list(
state: &AppState,
req: &Parts,
Expand Down
10 changes: 6 additions & 4 deletions src/controllers/helpers/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::util::errors::{bad_request, AppResult};
use crate::util::HeaderMapExt;
use std::num::NonZeroU32;

use axum::extract::FromRequestParts;
use base64::{engine::general_purpose, Engine};
use diesel::pg::Pg;
use diesel::prelude::*;
Expand Down Expand Up @@ -55,19 +56,20 @@ impl PaginationOptions {
}
}

#[derive(Debug, Deserialize, utoipa::IntoParams)]
#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
#[from_request(via(axum::extract::Query))]
#[into_params(parameter_in = Query)]
pub struct PaginationQueryParams {
/// The page number to request.
///
/// This parameter is mutually exclusive with `seek` and not supported for
/// all requests.
#[param(value_type = Option<u32>, minimum = 1)]
page: Option<NonZeroU32>,
pub page: Option<NonZeroU32>,

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

/// The seek key to request.
///
Expand All @@ -76,7 +78,7 @@ pub struct PaginationQueryParams {
///
/// The seek key can usually be found in the `meta.next_page` field of
/// paginated responses.
seek: Option<String>,
pub seek: Option<String>,
}

pub(crate) struct PaginationOptionsBuilder {
Expand Down
37 changes: 27 additions & 10 deletions src/controllers/krate/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,30 @@ use crate::models::{
};
use crate::schema::*;
use crate::util::errors::{bad_request, crate_not_found, AppResult, BoxedAppError};
use crate::util::RequestUtils;
use crate::views::{EncodableCategory, EncodableCrate, EncodableKeyword, EncodableVersion};
use axum::extract::{FromRequestParts, Query};
use axum_extra::json;
use axum_extra::response::ErasedJson;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use http::request::Parts;
use std::cmp::Reverse;
use std::str::FromStr;

#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
#[from_request(via(Query))]
#[into_params(parameter_in = Query)]
pub struct FindQueryParams {
/// Additional data to include in the response.
///
/// Valid values: `versions`, `keywords`, `categories`, `badges`,
/// `downloads`, or `full`.
///
/// Defaults to `full` for backwards compatibility.
///
/// This parameter expects a comma-separated list of values.
include: Option<String>,
}

/// Get crate metadata (for the `new` crate).
///
/// This endpoint works around a small limitation in `axum` and is delegating
Expand All @@ -32,26 +46,29 @@ use std::str::FromStr;
tag = "crates",
responses((status = 200, description = "Successful Response")),
)]
pub async fn find_new_crate(app: AppState, req: Parts) -> AppResult<ErasedJson> {
pub async fn find_new_crate(app: AppState, params: FindQueryParams) -> AppResult<ErasedJson> {
let name = "new".to_string();
find_crate(app, CratePath { name }, req).await
find_crate(app, CratePath { name }, params).await
}

/// Get crate metadata.
#[utoipa::path(
get,
path = "/api/v1/crates/{name}",
params(CratePath),
params(CratePath, FindQueryParams),
tag = "crates",
responses((status = 200, description = "Successful Response")),
)]
pub async fn find_crate(app: AppState, path: CratePath, req: Parts) -> AppResult<ErasedJson> {
pub async fn find_crate(
app: AppState,
path: CratePath,
params: FindQueryParams,
) -> AppResult<ErasedJson> {
let mut conn = app.db_read().await?;

let include = req
.query()
.get("include")
.map(|mode| ShowIncludeMode::from_str(mode))
let include = params
.include
.map(|mode| ShowIncludeMode::from_str(&mode))
.transpose()?
.unwrap_or_default();

Expand Down
56 changes: 42 additions & 14 deletions src/controllers/krate/versions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Endpoint for versions of a crate
use axum::extract::{FromRequestParts, Query};
use axum_extra::json;
use axum_extra::response::ErasedJson;
use diesel::dsl::not;
Expand All @@ -11,48 +12,75 @@ use indexmap::{IndexMap, IndexSet};
use std::str::FromStr;

use crate::app::AppState;
use crate::controllers::helpers::pagination::{encode_seek, Page, PaginationOptions};
use crate::controllers::helpers::pagination::{
encode_seek, Page, PaginationOptions, PaginationQueryParams,
};
use crate::controllers::krate::CratePath;
use crate::models::{User, Version, VersionOwnerAction};
use crate::schema::{users, versions};
use crate::util::errors::{bad_request, AppResult, BoxedAppError};
use crate::util::RequestUtils;
use crate::views::EncodableVersion;

#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)]
#[from_request(via(Query))]
#[into_params(parameter_in = Query)]
pub struct ListQueryParams {
/// Additional data to include in the response.
///
/// Valid values: `release_tracks`.
///
/// Defaults to no additional data.
///
/// This parameter expects a comma-separated list of values.
include: Option<String>,

/// The sort order of the versions.
///
/// Valid values: `date`, and `semver`.
///
/// Defaults to `semver`.
sort: Option<String>,
}

/// List all versions of a crate.
#[utoipa::path(
get,
path = "/api/v1/crates/{name}/versions",
params(CratePath),
params(CratePath, ListQueryParams, PaginationQueryParams),
tag = "versions",
responses((status = 200, description = "Successful Response")),
)]
pub async fn list_versions(state: AppState, path: CratePath, req: Parts) -> AppResult<ErasedJson> {
pub async fn list_versions(
state: AppState,
path: CratePath,
params: ListQueryParams,
pagination: PaginationQueryParams,
req: Parts,
) -> AppResult<ErasedJson> {
let mut conn = state.db_read().await?;

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

let mut pagination = None;
let params = req.query();
// To keep backward compatibility, we paginate only if per_page is provided
if params.get("per_page").is_some() {
pagination = Some(
let pagination = match pagination.per_page {
Some(_) => Some(
PaginationOptions::builder()
.enable_seek(true)
.enable_pages(false)
.gather(&req)?,
);
}
),
None => None,
};

let include = req
.query()
.get("include")
.map(|mode| ShowIncludeMode::from_str(mode))
let include = params
.include
.map(|mode| ShowIncludeMode::from_str(&mode))
.transpose()?
.unwrap_or_default();

// Sort by semver by default
let versions_and_publishers = match params.get("sort").map(|s| s.to_lowercase()).as_deref() {
let versions_and_publishers = match params.sort.map(|s| s.to_lowercase()).as_deref() {
Some("date") => {
list_by_date(crate_id, pagination.as_ref(), include, &req, &mut conn).await?
}
Expand Down
Loading

0 comments on commit cdba41c

Please sign in to comment.