refactor: deps cleanup, split openapi, extract api-types crate

This commit is contained in:
2026-05-12 11:54:00 +02:00
parent 2d6121239f
commit 99ce81efe5
46 changed files with 695 additions and 808 deletions

View File

@@ -31,19 +31,22 @@ use domain::{
};
#[cfg(feature = "federation")]
use crate::dtos::{ActorListResponse, ActorUrlRequest, FollowRequest, RemoteActorDto};
use api_types::{
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
BlockedDomainResponse, FollowRequest, RemoteActorDto,
};
use api_types::{
ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams, DiaryResponse,
DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewRequest, LoginRequest, LoginResponse,
MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieStatsDto,
PaginationQueryParams, ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse,
SocialFeedResponse, SocialReviewDto, UserProfileQueryParams, UserProfileResponse, UserStatsDto,
UserSummaryDto, UserTrendsDto, UsersResponse,
};
use crate::{
dtos::{
ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams,
DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewData,
LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto, MonthlyRatingDto,
MovieDetailResponse, MovieDto, MovieStatsDto, PaginationQueryParams, ProfileResponse,
RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto,
UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto,
UsersResponse,
},
errors::ApiError,
extractors::AuthenticatedUser,
forms::{to_diary_query, LogReviewData},
state::AppState,
};
@@ -60,7 +63,7 @@ pub async fn get_diary(
State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> {
let page = get_diary::execute(&state.app_ctx, params.into()).await?;
let page = get_diary::execute(&state.app_ctx, to_diary_query(params)).await?;
Ok(Json(DiaryResponse {
items: page.items.iter().map(entry_to_dto).collect(),
@@ -415,7 +418,7 @@ fn entry_to_dto(entry: &DiaryEntry) -> DiaryEntryDto {
#[utoipa::path(
get, path = "/api/v1/admin/blocked-domains",
responses(
(status = 200, body = Vec<crate::dtos::BlockedDomainResponse>),
(status = 200, body = Vec<BlockedDomainResponse>),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden — admin only"),
),
@@ -427,9 +430,9 @@ pub async fn get_blocked_domains_admin(
) -> impl IntoResponse {
match state.ap_service.get_blocked_domains().await {
Ok(domains) => {
let response: Vec<crate::dtos::BlockedDomainResponse> = domains
let response: Vec<BlockedDomainResponse> = domains
.into_iter()
.map(|d| crate::dtos::BlockedDomainResponse {
.map(|d| BlockedDomainResponse {
domain: d.domain,
reason: d.reason,
blocked_at: d.blocked_at,
@@ -444,7 +447,7 @@ pub async fn get_blocked_domains_admin(
#[cfg(feature = "federation")]
#[utoipa::path(
post, path = "/api/v1/admin/blocked-domains",
request_body = crate::dtos::AddBlockedDomainRequest,
request_body = AddBlockedDomainRequest,
responses(
(status = 201, description = "Domain blocked"),
(status = 401, description = "Unauthorized"),
@@ -455,7 +458,7 @@ pub async fn get_blocked_domains_admin(
pub async fn add_blocked_domain_admin(
State(state): State<AppState>,
_admin: crate::extractors::AdminUser,
axum::Json(body): axum::Json<crate::dtos::AddBlockedDomainRequest>,
axum::Json(body): axum::Json<AddBlockedDomainRequest>,
) -> impl IntoResponse {
match state.ap_service.add_blocked_domain(&body.domain, body.reason.as_deref()).await {
Ok(()) => StatusCode::CREATED.into_response(),
@@ -531,7 +534,7 @@ pub async fn unblock_actor_api(
#[utoipa::path(
get, path = "/api/v1/social/blocked",
responses(
(status = 200, body = Vec<crate::dtos::BlockedActorResponse>),
(status = 200, body = Vec<BlockedActorResponse>),
(status = 401, description = "Unauthorized"),
),
security(("bearer_auth" = []))
@@ -542,9 +545,9 @@ pub async fn get_blocked_actors_api(
) -> impl IntoResponse {
match state.ap_service.get_blocked_actors(user.0.value()).await {
Ok(actors) => {
let response: Vec<crate::dtos::BlockedActorResponse> = actors
let response: Vec<BlockedActorResponse> = actors
.into_iter()
.map(|a| crate::dtos::BlockedActorResponse {
.map(|a| BlockedActorResponse {
url: a.url,
handle: a.handle,
display_name: a.display_name,

View File

@@ -30,12 +30,10 @@ use domain::models::ExportFormat;
use domain::{errors::DomainError, value_objects::UserId};
#[cfg(feature = "federation")]
use crate::dtos::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm};
use crate::forms::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm};
use crate::{
csrf::CsrfToken,
dtos::{
ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm,
},
forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm},
extractors::{AdminUser, OptionalCookieUser, RequiredCookieUser},
state::AppState,
};
@@ -280,7 +278,7 @@ pub async fn post_delete_review(
RequiredCookieUser(user_id): RequiredCookieUser,
Extension(csrf): Extension<CsrfToken>,
Path(review_id): Path<Uuid>,
Form(form): Form<crate::dtos::DeleteRedirectForm>,
Form(form): Form<crate::forms::DeleteRedirectForm>,
) -> impl IntoResponse {
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
return StatusCode::FORBIDDEN.into_response();
@@ -311,7 +309,7 @@ pub async fn post_delete_review(
pub async fn get_export(
State(state): State<AppState>,
RequiredCookieUser(user_id): RequiredCookieUser,
Query(params): Query<crate::dtos::ExportQueryParams>,
Query(params): Query<api_types::ExportQueryParams>,
) -> impl IntoResponse {
let format = match params.format.as_str() {
"csv" => ExportFormat::Csv,
@@ -504,7 +502,7 @@ pub async fn get_user_profile(
State(state): State<AppState>,
Path(profile_user_uuid): Path<Uuid>,
headers: axum::http::HeaderMap,
Query(params): Query<crate::dtos::ProfileQueryParams>,
Query(params): Query<crate::forms::ProfileQueryParams>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
// Content negotiation: AP clients request application/activity+json
@@ -800,7 +798,7 @@ pub async fn get_following_page(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
Path(profile_user_uuid): Path<Uuid>,
Query(params): Query<crate::dtos::ErrorQuery>,
Query(params): Query<crate::forms::ErrorQuery>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
if user_id.value() != profile_user_uuid {
@@ -850,7 +848,7 @@ pub async fn get_followers_page(
RequiredCookieUser(user_id): RequiredCookieUser,
State(state): State<AppState>,
Path(profile_user_uuid): Path<Uuid>,
Query(params): Query<crate::dtos::ErrorQuery>,
Query(params): Query<crate::forms::ErrorQuery>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
if user_id.value() != profile_user_uuid {
@@ -935,7 +933,7 @@ pub async fn get_movie_detail(
OptionalCookieUser(user_id): OptionalCookieUser,
State(state): State<AppState>,
Path(movie_id): Path<uuid::Uuid>,
Query(params): Query<crate::dtos::PaginationQueryParams>,
Query(params): Query<api_types::PaginationQueryParams>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
let ctx = build_page_context(&state, user_id, csrf.0).await;

View File

@@ -4,7 +4,11 @@ use axum::{
http::StatusCode,
response::{Html, IntoResponse, Redirect},
};
use serde::{Deserialize, Serialize};
use api_types::{
ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, SessionCreatedResponse,
SessionStateResponse,
};
use serde::Deserialize;
use std::collections::HashMap;
use application::{
@@ -465,13 +469,6 @@ pub async fn get_import_done(
// ── REST API handlers ──────────────────────────────────────────────────────
#[derive(Serialize, utoipa::ToSchema)]
pub struct SessionCreatedResponse {
pub session_id: String,
pub columns: Vec<String>,
pub sample_rows: Vec<Vec<String>>,
}
#[utoipa::path(
post, path = "/api/v1/import/sessions",
request_body(content_type = "multipart/form-data", description = "file (binary) + format (csv|json|xlsx)"),
@@ -544,14 +541,6 @@ pub async fn api_post_session(
}
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct SessionStateResponse {
pub session_id: String,
pub columns: Vec<String>,
pub has_mappings: bool,
pub row_count: usize,
}
#[utoipa::path(
get, path = "/api/v1/import/sessions/{id}",
params(("id" = String, Path, description = "Import session UUID")),
@@ -607,23 +596,6 @@ pub async fn api_get_session(
}
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ApiFieldMapping {
/// Column name in the source file
pub source_column: String,
/// Domain field: title | release_year | director | rating | watched_at | comment | external_metadata_id
pub domain_field: String,
/// For rating fields: multiply raw value by this factor (e.g. 0.5 for 10-point → 5-point scale)
pub rating_scale: Option<f64>,
/// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y")
pub date_format: Option<String>,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ApplyMappingRequest {
pub mappings: Vec<ApiFieldMapping>,
}
#[utoipa::path(
put, path = "/api/v1/import/sessions/{id}/mapping",
params(("id" = String, Path, description = "Import session UUID")),
@@ -692,12 +664,6 @@ pub async fn api_put_mapping(
}
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ConfirmRequest {
/// Indices (0-based) of rows from the mapping preview to import
pub confirmed_indices: Vec<usize>,
}
#[utoipa::path(
post, path = "/api/v1/import/sessions/{id}/confirm",
params(("id" = String, Path, description = "Import session UUID")),
@@ -776,14 +742,6 @@ pub async fn api_get_profiles(
}
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct SaveProfileRequest {
/// Session UUID whose current field_mappings to save
pub session_id: String,
/// Human-readable profile name (e.g. "Letterboxd")
pub name: String,
}
#[utoipa::path(
post, path = "/api/v1/import/profiles",
request_body = SaveProfileRequest,