feat(openapi): upgrade code to gnerate openapi on all path

This commit is contained in:
Edouard Vanbelle
2026-04-27 22:59:18 +02:00
parent 05d3b2abaa
commit d0c6bd6a73
17 changed files with 1723 additions and 191 deletions
+124 -9
View File
@@ -6,6 +6,7 @@ use axum::{
};
use serde::Serialize;
use tokio::io::AsyncWriteExt;
use utoipa::ToSchema;
use crate::application::ports::dedup_ports::DedupResultDto;
use crate::common::di::AppState;
@@ -16,7 +17,7 @@ use std::sync::Arc;
type GlobalState = Arc<AppState>;
/// Response for hash check endpoint
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, ToSchema)]
pub struct HashCheckResponse {
/// Whether a blob with this hash already exists
pub exists: bool,
@@ -31,7 +32,7 @@ pub struct HashCheckResponse {
}
/// Response for upload with dedup endpoint
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, ToSchema)]
pub struct DedupUploadResponse {
/// Whether this was a new file or an existing one
pub is_new: bool,
@@ -46,7 +47,7 @@ pub struct DedupUploadResponse {
}
/// Response for dedup stats endpoint
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, ToSchema)]
pub struct StatsResponse {
/// Total number of unique blobs stored
pub unique_blobs: u64,
@@ -64,8 +65,10 @@ pub struct StatsResponse {
pub savings_percentage: f64,
}
/// Handler for deduplication-related endpoints
/// Handler for deduplication-related endpoints.
///
/// All route functions are free functions at module scope — see the section
/// below the impl block for the reason (utoipa 5.4.0 limitation).
/// Provides endpoints for:
/// - Checking if content already exists (by hash)
/// - Uploading files with automatic deduplication
@@ -73,13 +76,19 @@ pub struct StatsResponse {
pub struct DedupHandler;
impl DedupHandler {
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
// Same utoipa 5.4.0 limitation as ChunkedUploadHandler: the macro generates
// helper structs inside its expansion and Rust forbids structs inside impl blocks.
// All route handlers are free functions below; they delegate to these *_impl methods.
// TODO: collapse back into the impl block after a utoipa upgrade.
/// Check if the authenticated user already has a file with the given hash.
///
/// User-scoped: only reveals whether **this user** owns a file that
/// references the blob — never exposes global existence or ref_count.
///
/// GET /api/dedup/check/{hash}
pub async fn check_hash(
pub(super) async fn check_hash_impl(
State(state): State<GlobalState>,
auth_user: AuthUser,
Path(hash): Path<String>,
@@ -142,7 +151,7 @@ impl DedupHandler {
/// the pre-computed hash so the file is never re-read for hashing.
///
/// POST /api/dedup/upload
pub async fn upload_with_dedup(
pub(super) async fn upload_with_dedup_impl(
State(state): State<GlobalState>,
_auth_user: AuthUser,
mut multipart: Multipart,
@@ -300,7 +309,7 @@ impl DedupHandler {
/// - Total references
/// - Bytes saved
/// - Deduplication ratio
pub async fn get_stats(
pub(super) async fn get_stats_impl(
State(state): State<GlobalState>,
auth_user: AuthUser,
) -> impl IntoResponse {
@@ -349,7 +358,7 @@ impl DedupHandler {
/// Returns the raw content of a blob **only if** the authenticated user
/// owns at least one file that references it. Returns 404 otherwise
/// (does not reveal whether the blob exists globally).
pub async fn get_blob(
pub(super) async fn get_blob_impl(
State(state): State<GlobalState>,
auth_user: AuthUser,
Path(hash): Path<String>,
@@ -423,7 +432,7 @@ impl DedupHandler {
///
/// Verifies integrity and returns current statistics.
/// Useful for health checks and auditing.
pub async fn recalculate_stats(
pub(super) async fn recalculate_stats_impl(
State(state): State<GlobalState>,
auth_user: AuthUser,
) -> impl IntoResponse {
@@ -485,6 +494,112 @@ impl DedupHandler {
}
}
// ── Route handlers (free functions) ──────────────────────────────────────────
//
// Same utoipa 5.4.0 limitation as ChunkedUploadHandler: #[utoipa::path] cannot
// be applied to methods on DedupHandler because the macro generates helper structs
// that Rust forbids inside impl blocks. All logic lives in the DedupHandler::*_impl
// methods above; these thin wrappers carry the OpenAPI annotation at module scope.
//
// routes.rs calls these free functions directly instead of DedupHandler::method.
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
#[utoipa::path(
get,
path = "/api/dedup/check/{hash}",
params(
("hash" = String, Path, description = "SHA-256 hash (64 hex characters)"),
),
responses(
(status = 200, description = "Hash check result (user-scoped)", body = HashCheckResponse),
(status = 400, description = "Invalid hash format"),
),
tag = "dedup",
security(("bearerAuth" = []))
)]
pub async fn check_hash(
state: State<GlobalState>,
auth_user: AuthUser,
path: Path<String>,
) -> impl IntoResponse {
DedupHandler::check_hash_impl(state, auth_user, path).await
}
#[utoipa::path(
post,
path = "/api/dedup/upload",
request_body(content_type = "multipart/form-data", description = "Multipart form with a 'file' field"),
responses(
(status = 201, description = "New blob stored", body = DedupUploadResponse),
(status = 200, description = "Blob already existed (dedup hit)", body = DedupUploadResponse),
(status = 400, description = "No file field or empty file"),
(status = 500, description = "Upload failed"),
),
tag = "dedup",
security(("bearerAuth" = []))
)]
pub async fn upload_with_dedup(
state: State<GlobalState>,
auth_user: AuthUser,
multipart: Multipart,
) -> impl IntoResponse {
DedupHandler::upload_with_dedup_impl(state, auth_user, multipart).await
}
#[utoipa::path(
get,
path = "/api/dedup/stats",
responses(
(status = 200, description = "Deduplication statistics", body = StatsResponse),
(status = 403, description = "Admin role required"),
),
tag = "dedup",
security(("bearerAuth" = []))
)]
pub async fn get_stats(state: State<GlobalState>, auth_user: AuthUser) -> impl IntoResponse {
DedupHandler::get_stats_impl(state, auth_user).await
}
#[utoipa::path(
get,
path = "/api/dedup/blob/{hash}",
params(
("hash" = String, Path, description = "SHA-256 hash of the blob (64 hex characters)"),
),
responses(
(status = 200, description = "Raw blob content (user-scoped)"),
(status = 400, description = "Invalid hash format"),
(status = 404, description = "Blob not found or not owned by this user"),
),
tag = "dedup",
security(("bearerAuth" = []))
)]
pub async fn get_blob(
state: State<GlobalState>,
auth_user: AuthUser,
path: Path<String>,
) -> impl IntoResponse {
DedupHandler::get_blob_impl(state, auth_user, path).await
}
#[utoipa::path(
post,
path = "/api/dedup/recalculate",
responses(
(status = 200, description = "Statistics after integrity verification", body = StatsResponse),
(status = 403, description = "Admin role required"),
(status = 500, description = "Integrity verification failed"),
),
tag = "dedup",
security(("bearerAuth" = []))
)]
pub async fn recalculate_stats(
state: State<GlobalState>,
auth_user: AuthUser,
) -> impl IntoResponse {
DedupHandler::recalculate_stats_impl(state, auth_user).await
}
#[cfg(test)]
mod tests {
use super::*;