From 35fadb46f60c9045297ed812d9110fbf7cd3713c Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:16:09 +0800 Subject: [PATCH] feat: add pagination helper and integrate serde_urlencoded for query extraction --- Cargo.lock | 1 + apps/api/Cargo.toml | 3 +- apps/api/src/routes/api.rs | 1 + apps/api/src/routes/api/helper.rs | 1 + apps/api/src/routes/api/helper/pagination.rs | 65 ++++++++++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/routes/api/helper.rs create mode 100644 apps/api/src/routes/api/helper/pagination.rs diff --git a/Cargo.lock b/Cargo.lock index 5259b53..a4d3ce4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5446,6 +5446,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "serde_urlencoded", "tempfile", "tokio", "tower", diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index 81a05be..4af67d5 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -30,9 +30,10 @@ jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } uuid = { version = "1.19.0", features = ["v4", "serde", "fast-rng"] } tower-http = { version = "0.6.8", features = ["cors"] } reqwest = { version = "^0.12", features = ["json", "multipart", "stream"] } +serde_urlencoded = { version = "0.7.1" } [dev-dependencies] tempfile = "3" [lints.clippy] -unwrap_used = "deny" \ No newline at end of file +unwrap_used = "deny" diff --git a/apps/api/src/routes/api.rs b/apps/api/src/routes/api.rs index 3546a2b..451722e 100644 --- a/apps/api/src/routes/api.rs +++ b/apps/api/src/routes/api.rs @@ -1,5 +1,6 @@ mod auth; mod health; +mod helper; mod openapi; mod restricted; diff --git a/apps/api/src/routes/api/helper.rs b/apps/api/src/routes/api/helper.rs new file mode 100644 index 0000000..bc8665b --- /dev/null +++ b/apps/api/src/routes/api/helper.rs @@ -0,0 +1 @@ +pub mod pagination; diff --git a/apps/api/src/routes/api/helper/pagination.rs b/apps/api/src/routes/api/helper/pagination.rs new file mode 100644 index 0000000..4228be0 --- /dev/null +++ b/apps/api/src/routes/api/helper/pagination.rs @@ -0,0 +1,65 @@ +use axum::{ + extract::FromRequestParts, + http::{StatusCode, request::Parts}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, utoipa::ToSchema)] +/// Pagination parameters for API requests +pub struct Pagination { + /// Page number (1-based) + pub page: u32, + /// Items per page + pub per_page: u32, +} + +impl Default for Pagination { + fn default() -> Self { + Self { + page: 1, + per_page: 20, + } + } +} + +#[derive(Serialize, Deserialize, utoipa::ToSchema)] +/// Pagination information included in API responses +pub struct PaginationInfo { + /// Total number of items + pub total_items: u64, + /// Total number of pages + pub total_pages: u32, + /// Current page number + pub current_page: u32, + /// Items per page + pub per_page: u32, +} + +/// Extractor for pagination parameters from query string +pub struct ExtractPagination(pub Pagination); + +impl FromRequestParts for ExtractPagination +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let query = parts.uri.query().unwrap_or(""); + let pagination: Pagination = serde_urlencoded::from_str(query).unwrap_or_default(); + + // validation + if pagination.page == 0 { + return Err((StatusCode::BAD_REQUEST, "page must be greater than 0")); + } + + if pagination.per_page < 1 || pagination.per_page > 100 { + return Err(( + StatusCode::BAD_REQUEST, + "per_page must be between 1 and 100", + )); + } + + Ok(ExtractPagination(pagination)) + } +}