From 3b16b165bbcdd0c6e16873cba73dc942bb98c494 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Wed, 15 Oct 2025 06:11:57 +0800 Subject: [PATCH] feat: enhance error handling and API documentation for image and label features --- .../feature/image/src/application/error/image_error.rs | 6 ++++++ .../image/src/framework/web/get_image_by_id_handler.rs | 10 +++++----- .../image/src/framework/web/upload_image_handler.rs | 10 +++++----- .../feature/label/src/application/error/label_error.rs | 8 +++++++- .../label/src/framework/web/create_label_handler.rs | 4 +++- .../label/src/framework/web/get_all_labels_handler.rs | 2 +- .../label/src/framework/web/update_label_handler.rs | 8 ++++---- .../feature/post/src/application/error/post_error.rs | 10 ++++++++-- backend/feature/post/src/domain/entity/post_info.rs | 2 +- .../post/src/framework/web/create_post_handler.rs | 3 +++ .../post/src/framework/web/get_post_by_id_handler.rs | 3 ++- .../post/src/framework/web/update_post_handler.rs | 5 ++++- backend/server/src/api_doc.rs | 2 ++ backend/server/src/main.rs | 1 + 14 files changed, 52 insertions(+), 22 deletions(-) diff --git a/backend/feature/image/src/application/error/image_error.rs b/backend/feature/image/src/application/error/image_error.rs index 0d046b2..b787725 100644 --- a/backend/feature/image/src/application/error/image_error.rs +++ b/backend/feature/image/src/application/error/image_error.rs @@ -7,6 +7,12 @@ pub enum ImageError { Unexpected(anyhow::Error), } +impl Into for ImageError { + fn into(self) -> String { + format!("{}", self) + } +} + impl Display for ImageError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/backend/feature/image/src/framework/web/get_image_by_id_handler.rs b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs index e345c8c..593ab8d 100644 --- a/backend/feature/image/src/framework/web/get_image_by_id_handler.rs +++ b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs @@ -15,7 +15,7 @@ use crate::{ summary = "Get image by ID", responses ( (status = 200, body = inline(ResponseBodySchema), content_type = "image/*"), - (status = 404, description = "Image not found") + (status = 404, description = ImageError::NotFound), ) )] pub async fn get_image_by_id_handler( @@ -31,12 +31,12 @@ pub async fn get_image_by_id_handler( .body(image_response.data), Err(e) => match e { ImageError::NotFound => HttpResponse::NotFound().finish(), - ImageError::Unexpected(e) => { - capture_anyhow(&e); + ImageError::UnsupportedMimeType(_) => { + capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - _ => { - capture_anyhow(&anyhow!(e)); + ImageError::Unexpected(e) => { + capture_anyhow(&e); HttpResponse::InternalServerError().finish() } }, diff --git a/backend/feature/image/src/framework/web/upload_image_handler.rs b/backend/feature/image/src/framework/web/upload_image_handler.rs index 3964a8f..faaf2f1 100644 --- a/backend/feature/image/src/framework/web/upload_image_handler.rs +++ b/backend/feature/image/src/framework/web/upload_image_handler.rs @@ -25,7 +25,7 @@ use crate::{ ), responses ( (status = 201, body = ImageInfoResponseDto), - (status = 400, description = "Unsupported MIME type or file field not found"), + (status = 400, description = ImageError::UnsupportedMimeType("{MIME_TYPE}".to_string())), ), security( ("oauth2" = []) @@ -78,12 +78,12 @@ pub async fn upload_image_handler( ImageError::UnsupportedMimeType(mime_type) => { HttpResponse::BadRequest().body(format!("Unsupported MIME type: {}", mime_type)) } - ImageError::Unexpected(e) => { - capture_anyhow(&e); + ImageError::NotFound => { + capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } - _ => { - capture_anyhow(&anyhow!(e)); + ImageError::Unexpected(e) => { + capture_anyhow(&e); HttpResponse::InternalServerError().finish() } }, diff --git a/backend/feature/label/src/application/error/label_error.rs b/backend/feature/label/src/application/error/label_error.rs index 594bdf2..3ff2707 100644 --- a/backend/feature/label/src/application/error/label_error.rs +++ b/backend/feature/label/src/application/error/label_error.rs @@ -8,11 +8,17 @@ pub enum LabelError { Unexpected(anyhow::Error), } +impl Into for LabelError { + fn into(self) -> String { + format!("{}", self) + } +} + impl Display for LabelError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LabelError::NotFound => write!(f, "Label not found"), - LabelError::Unauthorized => write!(f, "Unauthorized access to label"), + LabelError::Unauthorized => write!(f, "Unauthorized access"), LabelError::DuplicatedLabelName => write!(f, "Label name already exists"), LabelError::Unexpected(e) => write!(f, "Unexpected error: {}", e), } diff --git a/backend/feature/label/src/framework/web/create_label_handler.rs b/backend/feature/label/src/framework/web/create_label_handler.rs index d043631..fe36657 100644 --- a/backend/feature/label/src/framework/web/create_label_handler.rs +++ b/backend/feature/label/src/framework/web/create_label_handler.rs @@ -14,10 +14,12 @@ use crate::{ #[utoipa::path( post, path = "/label", - tag = "post", + tag = "label", summary = "Create a new label", responses( (status = 201, body = LabelResponseDto), + (status = 401, description = LabelError::Unauthorized), + (status = 409, description = LabelError::DuplicatedLabelName), ), security( ("oauth2" = []) diff --git a/backend/feature/label/src/framework/web/get_all_labels_handler.rs b/backend/feature/label/src/framework/web/get_all_labels_handler.rs index a329cc9..455f708 100644 --- a/backend/feature/label/src/framework/web/get_all_labels_handler.rs +++ b/backend/feature/label/src/framework/web/get_all_labels_handler.rs @@ -10,7 +10,7 @@ use crate::{ #[utoipa::path( get, path = "/label", - tag = "post", + tag = "label", summary = "Get all labels", responses( (status = 200, body = Vec) diff --git a/backend/feature/label/src/framework/web/update_label_handler.rs b/backend/feature/label/src/framework/web/update_label_handler.rs index 4562137..5271bb0 100644 --- a/backend/feature/label/src/framework/web/update_label_handler.rs +++ b/backend/feature/label/src/framework/web/update_label_handler.rs @@ -13,13 +13,13 @@ use crate::{ #[utoipa::path( put, path = "/label/{id}", - tag = "post", + tag = "label", summary = "Update a label by ID", responses( (status = 200, body = LabelResponseDto), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Label not found"), - (status = 409, description = "Duplicated label name"), + (status = 401, description = LabelError::Unauthorized), + (status = 404, description = LabelError::NotFound), + (status = 409, description = LabelError::DuplicatedLabelName), ), security( ("oauth2" = []) diff --git a/backend/feature/post/src/application/error/post_error.rs b/backend/feature/post/src/application/error/post_error.rs index c67b2d7..e213e89 100644 --- a/backend/feature/post/src/application/error/post_error.rs +++ b/backend/feature/post/src/application/error/post_error.rs @@ -9,14 +9,20 @@ pub enum PostError { Unexpected(anyhow::Error), } +impl Into for PostError { + fn into(self) -> String { + format!("{}", self) + } +} + impl Display for PostError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PostError::NotFound => write!(f, "Post not found"), - PostError::Unauthorized => write!(f, "Unauthorized access to post"), + PostError::Unauthorized => write!(f, "Unauthorized access"), PostError::InvalidSemanticId => write!( f, - "Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`" + "Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_-]+$`" ), PostError::DuplicatedSemanticId => write!(f, "Semantic ID already exists"), PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e), diff --git a/backend/feature/post/src/domain/entity/post_info.rs b/backend/feature/post/src/domain/entity/post_info.rs index 402b31e..95b6e38 100644 --- a/backend/feature/post/src/domain/entity/post_info.rs +++ b/backend/feature/post/src/domain/entity/post_info.rs @@ -20,7 +20,7 @@ impl PostInfo { return Err(PostError::InvalidSemanticId); } - let re = Regex::new(r"^[0-9a-zA-Z_\-]+$").unwrap(); + let re = Regex::new(r"^[0-9a-zA-Z_-]+$").unwrap(); if !re.is_match(&self.semantic_id) { return Err(PostError::InvalidSemanticId); } diff --git a/backend/feature/post/src/framework/web/create_post_handler.rs b/backend/feature/post/src/framework/web/create_post_handler.rs index 0c1edec..15af5ed 100644 --- a/backend/feature/post/src/framework/web/create_post_handler.rs +++ b/backend/feature/post/src/framework/web/create_post_handler.rs @@ -18,6 +18,9 @@ use crate::{ summary = "Create a new post", responses( (status = 201, body = PostResponseDto), + (status = 400, description = PostError::InvalidSemanticId), + (status = 401, description = PostError::Unauthorized), + (status = 409, description = PostError::DuplicatedSemanticId), ), security( ("oauth2" = []) diff --git a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs index b66dabb..da4227b 100644 --- a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs +++ b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs @@ -16,7 +16,8 @@ use crate::{ description = "Only authenticated users can access unpublished posts. Accepts either numeric ID or semantic ID.", responses ( (status = 200, body = PostResponseDto), - (status = 404, description = "Post not found") + (status = 401, description = PostError::Unauthorized), + (status = 404, description = PostError::NotFound), ) )] pub async fn get_post_by_id_handler( diff --git a/backend/feature/post/src/framework/web/update_post_handler.rs b/backend/feature/post/src/framework/web/update_post_handler.rs index 24f8a7b..28cab9a 100644 --- a/backend/feature/post/src/framework/web/update_post_handler.rs +++ b/backend/feature/post/src/framework/web/update_post_handler.rs @@ -17,7 +17,10 @@ use crate::{ summary = "Update a post by ID", responses( (status = 200, body = PostResponseDto), - (status = 404, description = "Post not found"), + (status = 400, description = PostError::InvalidSemanticId), + (status = 401, description = PostError::Unauthorized), + (status = 404, description = PostError::NotFound), + (status = 409, description = PostError::DuplicatedSemanticId), ), security( ("oauth2" = []) diff --git a/backend/server/src/api_doc.rs b/backend/server/src/api_doc.rs index e4d2e2a..0f6c619 100644 --- a/backend/server/src/api_doc.rs +++ b/backend/server/src/api_doc.rs @@ -1,6 +1,7 @@ use actix_web::web; use auth::framework::web::auth_api_doc; use image::framework::web::image_api_doc; +use label::framework::web::label_api_doc; use post::framework::web::post_api_doc; use utoipa::{ OpenApi, @@ -40,6 +41,7 @@ pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { let openapi = ApiDoc::openapi() .merge_from(auth_api_doc::openapi()) .merge_from(image_api_doc::openapi()) + .merge_from(label_api_doc::openapi()) .merge_from(post_api_doc::openapi()); cfg.service(Redoc::with_url("/redoc", openapi)); diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 4d51d4d..c0e2cdb 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -85,6 +85,7 @@ fn create_app( .wrap(session_middleware_builder.build()) .app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.image_controller)) + .app_data(web::Data::from(container.label_controller)) .app_data(web::Data::from(container.post_controller)) .configure(configure_api_doc_routes) .configure(configure_auth_routes)