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
@@ -9,13 +9,14 @@
use axum::{
Json,
extract::{Path, Query, State},
extract::{Path, Query, Request, State},
http::{HeaderMap, StatusCode, header},
response::{IntoResponse, Response},
};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use utoipa::ToSchema;
use crate::application::ports::chunked_upload_ports::ChunkedUploadPort;
use crate::application::ports::chunked_upload_ports::DEFAULT_CHUNK_SIZE;
@@ -26,7 +27,7 @@ use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
/// Request body for creating an upload session
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateUploadRequest {
pub filename: String,
pub folder_id: Option<String>,
@@ -43,7 +44,7 @@ pub struct ChunkUploadParams {
}
/// Final response after completing upload
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, ToSchema)]
pub struct CompleteUploadResponse {
pub file_id: String,
pub filename: String,
@@ -52,9 +53,21 @@ pub struct CompleteUploadResponse {
}
/// Chunked Upload Handler
///
/// The handler struct exists as a named grouping. All route functions are free
/// functions at module scope — see the section below the impl block for the reason.
pub struct ChunkedUploadHandler;
impl ChunkedUploadHandler {
// ── Why no #[utoipa::path] here? ─────────────────────────────────────────────
// utoipa 5.4.0's proc macro generates helper structs / impls inside its expansion.
// Rust allows struct definitions at module scope but forbids them inside impl blocks,
// so `#[utoipa::path]` fails on every method in this impl block regardless of HTTP
// verb or annotation content. The same macro works fine on FileHandler / FolderHandler
// (root cause in utoipa unknown — likely a 5.4.x bug). All five route handlers are
// therefore declared as free functions below, which delegate to these `*_impl` methods.
// TODO: try removing free-function indirection after a utoipa upgrade.
/// POST /api/uploads - Create a new upload session
///
/// Request body:
@@ -77,7 +90,7 @@ impl ChunkedUploadHandler {
/// "expires_at": 86400
/// }
/// ```
pub async fn create_upload(
pub(super) async fn create_upload_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Json(request): Json<CreateUploadRequest>,
@@ -171,7 +184,7 @@ impl ChunkedUploadHandler {
/// - checksum: Optional MD5 checksum for verification
///
/// Body: Raw bytes of the chunk
pub async fn upload_chunk(
pub(super) async fn upload_chunk_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
@@ -220,7 +233,7 @@ impl ChunkedUploadHandler {
/// HEAD /api/uploads/:upload_id - Get upload status
///
/// Returns upload progress and pending chunks
pub async fn get_upload_status(
pub(super) async fn get_upload_status_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
@@ -251,7 +264,7 @@ impl ChunkedUploadHandler {
/// POST /api/uploads/:upload_id/complete - Finalize upload
///
/// Assembles all chunks into the final file and creates the file record
pub async fn complete_upload(
pub(super) async fn complete_upload_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
@@ -324,7 +337,7 @@ impl ChunkedUploadHandler {
/// DELETE /api/uploads/:upload_id - Cancel upload
///
/// Cancels an in-progress upload and cleans up temp files
pub async fn cancel_upload(
pub(super) async fn cancel_upload_impl(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
@@ -342,3 +355,134 @@ impl ChunkedUploadHandler {
}
}
}
// ── Route handlers (free functions) ──────────────────────────────────────────
//
// All five route functions live here rather than as methods on ChunkedUploadHandler
// because utoipa 5.4.0's #[utoipa::path] macro generates helper structs inside its
// expansion. Rust allows struct definitions at module scope but forbids them inside
// impl blocks — so every #[utoipa::path] annotation on a ChunkedUploadHandler method
// fails to compile regardless of HTTP verb or annotation content.
//
// FileHandler and FolderHandler are not affected (root cause in utoipa unknown, likely
// a 5.4.x regression). All logic lives in the ChunkedUploadHandler::*_impl methods
// above; these thin wrappers exist solely to carry the OpenAPI annotation at a scope
// where utoipa can generate its helper types.
//
// routes.rs calls these free functions directly.
// TODO: collapse back into the impl block after a utoipa upgrade resolves the issue.
#[utoipa::path(
post,
path = "/api/uploads",
request_body(content = CreateUploadRequest, content_type = "application/json", description = "Upload session parameters"),
responses(
(status = 201, description = "Upload session created", body = crate::application::ports::chunked_upload_ports::CreateUploadResponseDto),
(status = 400, description = "Invalid request (empty filename, zero size, chunk too small)"),
(status = 507, description = "Storage quota exceeded"),
),
tag = "uploads",
security(("bearerAuth" = []))
)]
pub async fn create_upload(
state: State<Arc<AppState>>,
auth_user: AuthUser,
request: Json<CreateUploadRequest>,
) -> impl IntoResponse {
ChunkedUploadHandler::create_upload_impl(state, auth_user, request).await
}
#[utoipa::path(
patch,
path = "/api/uploads/{upload_id}",
params(
("upload_id" = String, Path, description = "Upload session ID"),
("chunk_index" = usize, Query, description = "Zero-based chunk index"),
("checksum" = Option<String>, Query, description = "Optional MD5 checksum for integrity verification"),
),
request_body(content_type = "application/octet-stream", description = "Raw chunk bytes"),
responses(
(status = 200, description = "Chunk received", body = crate::application::ports::chunked_upload_ports::ChunkUploadResponseDto),
(status = 400, description = "Invalid chunk or checksum mismatch"),
(status = 404, description = "Upload session not found"),
),
tag = "uploads",
security(("bearerAuth" = []))
)]
pub async fn upload_chunk(
state: State<Arc<AppState>>,
auth_user: AuthUser,
path: Path<String>,
query: Query<ChunkUploadParams>,
headers: HeaderMap,
request: Request,
) -> impl IntoResponse {
let body = axum::body::to_bytes(request.into_body(), usize::MAX)
.await
.unwrap_or_default();
ChunkedUploadHandler::upload_chunk_impl(state, auth_user, path, query, headers, body).await
}
#[utoipa::path(
head,
path = "/api/uploads/{upload_id}",
params(
("upload_id" = String, Path, description = "Upload session ID"),
),
responses(
(status = 200, description = "Upload status in response headers and body", body = crate::application::ports::chunked_upload_ports::UploadStatusResponseDto),
(status = 404, description = "Upload session not found"),
),
tag = "uploads",
security(("bearerAuth" = []))
)]
pub async fn get_upload_status(
state: State<Arc<AppState>>,
auth_user: AuthUser,
path: Path<String>,
) -> impl IntoResponse {
ChunkedUploadHandler::get_upload_status_impl(state, auth_user, path).await
}
#[utoipa::path(
post,
path = "/api/uploads/{upload_id}/complete",
params(
("upload_id" = String, Path, description = "Upload session ID"),
),
responses(
(status = 201, description = "File assembled and created", body = CompleteUploadResponse),
(status = 404, description = "Upload session not found"),
(status = 500, description = "Assembly or file creation failed"),
),
tag = "uploads",
security(("bearerAuth" = []))
)]
pub async fn complete_upload(
state: State<Arc<AppState>>,
auth_user: AuthUser,
path: Path<String>,
) -> impl IntoResponse {
ChunkedUploadHandler::complete_upload_impl(state, auth_user, path).await
}
#[utoipa::path(
delete,
path = "/api/uploads/{upload_id}",
params(
("upload_id" = String, Path, description = "Upload session ID"),
),
responses(
(status = 204, description = "Upload cancelled and temp files cleaned up"),
(status = 500, description = "Cancel failed"),
),
tag = "uploads",
security(("bearerAuth" = []))
)]
pub async fn cancel_upload(
state: State<Arc<AppState>>,
auth_user: AuthUser,
path: Path<String>,
) -> impl IntoResponse {
ChunkedUploadHandler::cancel_upload_impl(state, auth_user, path).await
}