refactor: single refusal shape with openapi contract test

This commit is contained in:
2026-10-25 21:55:38 +01:00
parent 816d39ffd6
commit c8d9138349
25 changed files with 764 additions and 273 deletions

View File

@@ -1,3 +1,4 @@
use crate::extractors::{Body, Params, PathId};
use crate::support::build_export_response;
use crate::{
errors::ApiError,
@@ -14,12 +15,7 @@ use application::diary::{
delete_review, edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
queries::GetActivityFeedQuery,
};
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use uuid::Uuid;
#[utoipa::path(
@@ -27,13 +23,13 @@ use uuid::Uuid;
params(DiaryQueryParams),
responses(
(status = 200, body = DiaryResponse),
(status = 401, description = "Unauthorized"),
(status = 401, description = "Unauthorized", body = api_types::ErrorResponse),
),
security(("bearer_auth" = []))
)]
pub async fn get_diary(
State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>,
Params(params): Params<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> {
let page =
get_diary::execute(&state.app_ctx.deps.diary.get_diary, to_diary_query(params)).await?;
@@ -55,15 +51,15 @@ pub async fn get_diary(
request_body = LogReviewRequest,
responses(
(status = 201, description = "Review created"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized"),
(status = 400, description = "Invalid input", body = api_types::ErrorResponse),
(status = 401, description = "Unauthorized", body = api_types::ErrorResponse),
),
security(("bearer_auth" = []))
)]
pub async fn post_review(
State(state): State<AppState>,
user: AuthenticatedUser,
Json(req): Json<LogReviewRequest>,
Body(req): Body<LogReviewRequest>,
) -> Result<impl IntoResponse, ApiError> {
let data = LogReviewData::try_from(req).map_err(ApiError)?;
log_review::execute(
@@ -79,16 +75,16 @@ pub async fn post_review(
params(("id" = Uuid, Path, description = "Review ID")),
responses(
(status = 204, description = "Review deleted"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Review not found"),
(status = 401, description = "Unauthorized", body = api_types::ErrorResponse),
(status = 403, description = "Forbidden", body = api_types::ErrorResponse),
(status = 404, description = "Review not found", body = api_types::ErrorResponse),
),
security(("bearer_auth" = []))
)]
pub async fn delete_review(
State(state): State<AppState>,
AuthenticatedUser(user_id): AuthenticatedUser,
Path(review_id): Path<Uuid>,
PathId(review_id): PathId<Uuid>,
) -> Result<StatusCode, ApiError> {
let cmd = DeleteReviewCommand {
review_id,
@@ -104,18 +100,18 @@ pub async fn delete_review(
params(("id" = Uuid, Path, description = "Review ID")),
responses(
(status = 200, description = "Review updated"),
(status = 400, description = "Invalid input"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "Review not found"),
(status = 400, description = "Invalid input", body = api_types::ErrorResponse),
(status = 401, description = "Unauthorized", body = api_types::ErrorResponse),
(status = 403, description = "Forbidden", body = api_types::ErrorResponse),
(status = 404, description = "Review not found", body = api_types::ErrorResponse),
),
security(("bearer_auth" = []))
)]
pub async fn patch_review(
State(state): State<AppState>,
AuthenticatedUser(user_id): AuthenticatedUser,
Path(review_id): Path<Uuid>,
Json(req): Json<EditReviewRequest>,
PathId(review_id): PathId<Uuid>,
Body(req): Body<EditReviewRequest>,
) -> Result<StatusCode, ApiError> {
let watched_at = req
.watched_at
@@ -140,27 +136,37 @@ pub async fn patch_review(
params(ExportQueryParams),
responses(
(status = 200, description = "Diary file download", content_type = "text/csv"),
(status = 400, description = "Invalid format parameter"),
(status = 401, description = "Unauthorized"),
(status = 400, description = "Invalid format parameter", body = api_types::ErrorResponse),
(status = 401, description = "Unauthorized", body = api_types::ErrorResponse),
),
security(("bearer_auth" = []))
)]
pub async fn export_diary(
State(state): State<AppState>,
user: AuthenticatedUser,
Query(params): Query<ExportQueryParams>,
Params(params): Params<ExportQueryParams>,
) -> impl IntoResponse {
if !matches!(params.format.as_str(), "csv" | "json") {
return crate::errors::refuse(
StatusCode::BAD_REQUEST,
crate::errors::INVALID_INPUT,
"format must be csv or json",
);
}
build_export_response(&params.format, user.0.value(), &state)
}
#[utoipa::path(
get, path = "/api/v1/activity-feed",
params(ActivityFeedQueryParams),
responses((status = 200, body = ActivityFeedResponse)),
responses(
(status = 200, body = ActivityFeedResponse),
(status = 400, description = "Invalid query", body = api_types::ErrorResponse),
),
)]
pub async fn get_activity_feed(
State(state): State<AppState>,
Query(params): Query<ActivityFeedQueryParams>,
Params(params): Params<ActivityFeedQueryParams>,
) -> Result<Json<ActivityFeedResponse>, ApiError> {
let page = get_feed_uc::execute(
&state.app_ctx.deps.diary.get_activity_feed,