BLOG-103 feat: integrate Utoipa for API documentation and add image upload functionality
This commit is contained in:
parent
562d658ea1
commit
1cf634bd19
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -1669,6 +1669,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -11,5 +11,6 @@ futures.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
utoipa.workspace = true
|
||||
|
||||
auth.workspace = true
|
||||
|
@ -1,6 +1,7 @@
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ImageInfoResponseDto {
|
||||
pub id: i32,
|
||||
pub mime_type: String,
|
||||
|
@ -1 +1,5 @@
|
||||
pub mod image_api_doc;
|
||||
pub mod image_web_routes;
|
||||
|
||||
mod get_image_by_id_handler;
|
||||
mod upload_image_handler;
|
||||
|
@ -0,0 +1,43 @@
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
adapter::delivery::image_controller::ImageController,
|
||||
application::error::image_error::ImageError,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/image/{id}",
|
||||
tag = "image",
|
||||
summary = "Get image by ID",
|
||||
responses (
|
||||
(status = 200, body = inline(ResponseBodySchema), content_type = "image/*"),
|
||||
(status = 404, description = "Image not found")
|
||||
)
|
||||
)]
|
||||
pub async fn get_image_by_id_handler(
|
||||
image_controller: web::Data<dyn ImageController>,
|
||||
path: web::Path<i32>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
let result = image_controller.get_image_by_id(id).await;
|
||||
|
||||
match result {
|
||||
Ok(image_response) => HttpResponse::Ok()
|
||||
.content_type(image_response.mime_type)
|
||||
.body(image_response.data),
|
||||
Err(e) => match e {
|
||||
ImageError::NotFound => HttpResponse::NotFound().finish(),
|
||||
_ => {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ToSchema)]
|
||||
#[schema(value_type = String, format = Binary)]
|
||||
#[allow(dead_code)]
|
||||
struct ResponseBodySchema(Vec<u8>);
|
13
backend/feature/image/src/framework/web/image_api_doc.rs
Normal file
13
backend/feature/image/src/framework/web/image_api_doc.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use crate::framework::web::{get_image_by_id_handler, upload_image_handler};
|
||||
use utoipa::{OpenApi, openapi};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(paths(
|
||||
get_image_by_id_handler::get_image_by_id_handler,
|
||||
upload_image_handler::upload_image_handler
|
||||
))]
|
||||
struct ApiDoc;
|
||||
|
||||
pub fn openapi() -> openapi::OpenApi {
|
||||
ApiDoc::openapi()
|
||||
}
|
@ -1,11 +1,7 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use auth::framework::web::auth_middleware::UserId;
|
||||
use futures::StreamExt;
|
||||
use actix_web::web;
|
||||
|
||||
use crate::{
|
||||
adapter::delivery::{image_controller::ImageController, image_request_dto::ImageRequestDto},
|
||||
application::error::image_error::ImageError,
|
||||
use crate::framework::web::{
|
||||
get_image_by_id_handler::get_image_by_id_handler, upload_image_handler::upload_image_handler,
|
||||
};
|
||||
|
||||
pub fn configure_image_routes(cfg: &mut web::ServiceConfig) {
|
||||
@ -15,77 +11,3 @@ pub fn configure_image_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("/{id}", web::get().to(get_image_by_id_handler)),
|
||||
);
|
||||
}
|
||||
|
||||
async fn upload_image_handler(
|
||||
image_controller: web::Data<dyn ImageController>,
|
||||
mut payload: Multipart,
|
||||
_: UserId,
|
||||
) -> impl Responder {
|
||||
let mut image_request_dto: Option<ImageRequestDto> = None;
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut field = match item {
|
||||
Ok(field) => field,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
if field.name() != Some("file") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mime_type = field
|
||||
.content_type()
|
||||
.cloned()
|
||||
.map(|mt| mt.to_string())
|
||||
.unwrap_or_else(|| "application/octet-stream".to_string());
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(chunk) = field.next().await {
|
||||
match chunk {
|
||||
Ok(bytes) => data.extend_from_slice(&bytes),
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
image_request_dto = Some(ImageRequestDto { mime_type, data });
|
||||
break;
|
||||
}
|
||||
|
||||
let image_request_dto = match image_request_dto {
|
||||
Some(dto) => dto,
|
||||
None => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
let result = image_controller.upload_image(image_request_dto).await;
|
||||
|
||||
match result {
|
||||
Ok(image_info) => HttpResponse::Created().json(image_info),
|
||||
Err(e) => match e {
|
||||
ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")),
|
||||
_ => {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_image_by_id_handler(
|
||||
image_controller: web::Data<dyn ImageController>,
|
||||
path: web::Path<i32>,
|
||||
) -> impl Responder {
|
||||
let id = path.into_inner();
|
||||
let result = image_controller.get_image_by_id(id).await;
|
||||
|
||||
match result {
|
||||
Ok(image_response) => HttpResponse::Ok()
|
||||
.content_type(image_response.mime_type)
|
||||
.body(image_response.data),
|
||||
Err(e) => match e {
|
||||
ImageError::NotFound => HttpResponse::NotFound().finish(),
|
||||
_ => {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{HttpResponse, Responder, web};
|
||||
use auth::framework::web::auth_middleware::UserId;
|
||||
use futures::StreamExt;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
adapter::delivery::{
|
||||
image_controller::ImageController, image_info_response_dto::ImageInfoResponseDto,
|
||||
image_request_dto::ImageRequestDto,
|
||||
},
|
||||
application::error::image_error::ImageError,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/image/upload",
|
||||
tag = "image",
|
||||
summary = "Upload an image",
|
||||
request_body (
|
||||
content = RequestBodySchema,
|
||||
content_type = "multipart/form-data",
|
||||
),
|
||||
responses (
|
||||
(status = 201, body = ImageInfoResponseDto),
|
||||
(status = 400, description = "Unsupported MIME type or file field not found"),
|
||||
),
|
||||
security(
|
||||
("oauth2" = [])
|
||||
)
|
||||
)]
|
||||
pub async fn upload_image_handler(
|
||||
image_controller: web::Data<dyn ImageController>,
|
||||
mut payload: Multipart,
|
||||
_: UserId,
|
||||
) -> impl Responder {
|
||||
let mut image_request_dto: Option<ImageRequestDto> = None;
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
let mut field = match item {
|
||||
Ok(field) => field,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
if field.name() != Some("file") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mime_type = field
|
||||
.content_type()
|
||||
.cloned()
|
||||
.map(|mt| mt.to_string())
|
||||
.unwrap_or_else(|| "application/octet-stream".to_string());
|
||||
|
||||
let mut data = Vec::new();
|
||||
while let Some(chunk) = field.next().await {
|
||||
match chunk {
|
||||
Ok(bytes) => data.extend_from_slice(&bytes),
|
||||
Err(_) => return HttpResponse::InternalServerError().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
image_request_dto = Some(ImageRequestDto { mime_type, data });
|
||||
break;
|
||||
}
|
||||
|
||||
let image_request_dto = match image_request_dto {
|
||||
Some(dto) => dto,
|
||||
None => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
let result = image_controller.upload_image(image_request_dto).await;
|
||||
|
||||
match result {
|
||||
Ok(image_info) => HttpResponse::Created().json(image_info),
|
||||
Err(e) => match e {
|
||||
ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")),
|
||||
_ => {
|
||||
log::error!("{e:?}");
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(ToSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct RequestBodySchema {
|
||||
#[schema(value_type = String, format = Binary)]
|
||||
file: Vec<u8>,
|
||||
}
|
@ -3,5 +3,6 @@ use utoipa::IntoParams;
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct PostQueryDto {
|
||||
#[param(default = true)]
|
||||
pub is_published_only: Option<bool>,
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
use crate::framework::web::{get_all_post_info_handler, get_post_by_id_handler};
|
||||
use utoipa::{OpenApi, openapi};
|
||||
use crate::framework::web::{
|
||||
get_all_post_info_handler,
|
||||
get_post_by_id_handler
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(paths(
|
||||
get_all_post_info_handler::get_all_post_info_handler,
|
||||
get_post_by_id_handler::get_post_by_id_handler
|
||||
))]
|
||||
struct PostApiDoc;
|
||||
struct ApiDoc;
|
||||
|
||||
pub fn openapi() -> openapi::OpenApi {
|
||||
PostApiDoc::openapi()
|
||||
ApiDoc::openapi()
|
||||
}
|
||||
|
44
backend/server/src/api_doc.rs
Normal file
44
backend/server/src/api_doc.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use actix_web::web;
|
||||
use image::framework::web::image_api_doc;
|
||||
use post::framework::web::post_api_doc;
|
||||
use utoipa::{
|
||||
OpenApi,
|
||||
openapi::{
|
||||
Components, InfoBuilder, OpenApiBuilder,
|
||||
security::{AuthorizationCode, Flow, OAuth2, Scopes, SecurityScheme},
|
||||
},
|
||||
};
|
||||
use utoipa_redoc::{Redoc, Servable};
|
||||
|
||||
pub struct ApiDoc;
|
||||
|
||||
impl utoipa::OpenApi for ApiDoc {
|
||||
fn openapi() -> utoipa::openapi::OpenApi {
|
||||
let mut components = Components::new();
|
||||
|
||||
components.add_security_scheme(
|
||||
"oauth2",
|
||||
SecurityScheme::OAuth2(OAuth2::new(vec![Flow::AuthorizationCode(
|
||||
AuthorizationCode::new("/auth/login", "/auth/callback", Scopes::new()),
|
||||
)])),
|
||||
);
|
||||
|
||||
OpenApiBuilder::new()
|
||||
.info(
|
||||
InfoBuilder::new()
|
||||
.title("SquidSpirit API")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.build(),
|
||||
)
|
||||
.components(Some(components))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) {
|
||||
let openapi = ApiDoc::openapi()
|
||||
.merge_from(post_api_doc::openapi())
|
||||
.merge_from(image_api_doc::openapi());
|
||||
|
||||
cfg.service(Redoc::with_url("/redoc", openapi));
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
use actix_web::web;
|
||||
use post::framework::web::post_api_doc;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_redoc::{Redoc, Servable};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "SquidSpirit API",
|
||||
version = env!("CARGO_PKG_VERSION")
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) {
|
||||
let openapi = ApiDoc::openapi().merge_from(post_api_doc::openapi());
|
||||
|
||||
cfg.service(Redoc::with_url("/redoc", openapi));
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
pub mod apidoc;
|
||||
pub mod api_doc;
|
||||
pub mod configuration;
|
||||
pub mod container;
|
||||
|
@ -11,7 +11,7 @@ use auth::framework::web::auth_web_routes::configure_auth_routes;
|
||||
use image::framework::web::image_web_routes::configure_image_routes;
|
||||
use openidconnect::reqwest;
|
||||
use post::framework::web::post_web_routes::configure_post_routes;
|
||||
use server::{apidoc::configure_api_doc_routes, configuration::Configuration, container::Container};
|
||||
use server::{api_doc::configure_api_doc_routes, configuration::Configuration, container::Container};
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
#[actix_web::main]
|
||||
|
Loading…
x
Reference in New Issue
Block a user