feat: enhance error handling and API documentation for image and label features
All checks were successful
Frontend CI / build (push) Successful in 1m36s
PR Title Check / pr-title-check (pull_request) Successful in 16s

This commit is contained in:
SquidSpirit 2025-10-15 06:11:57 +08:00
parent 6213ab7a95
commit 3b16b165bb
14 changed files with 52 additions and 22 deletions

View File

@ -7,6 +7,12 @@ pub enum ImageError {
Unexpected(anyhow::Error),
}
impl Into<String> 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 {

View File

@ -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()
}
},

View File

@ -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()
}
},

View File

@ -8,11 +8,17 @@ pub enum LabelError {
Unexpected(anyhow::Error),
}
impl Into<String> 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),
}

View File

@ -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" = [])

View File

@ -10,7 +10,7 @@ use crate::{
#[utoipa::path(
get,
path = "/label",
tag = "post",
tag = "label",
summary = "Get all labels",
responses(
(status = 200, body = Vec<LabelResponseDto>)

View File

@ -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" = [])

View File

@ -9,14 +9,20 @@ pub enum PostError {
Unexpected(anyhow::Error),
}
impl Into<String> 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),

View File

@ -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);
}

View File

@ -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" = [])

View File

@ -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(

View File

@ -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" = [])

View File

@ -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));

View File

@ -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)