presentation wiring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 09:30:20 +02:00
parent 97a496553a
commit 79a06e6844
18 changed files with 883 additions and 18 deletions

View File

@@ -0,0 +1,255 @@
pub mod html {
use axum::{
extract::{Query, State},
response::{Html, IntoResponse, Redirect},
Form,
};
use chrono::NaiveDateTime;
use application::{
commands::LogReviewCommand,
queries::GetDiaryQuery,
use_cases::{get_diary, log_review},
};
use domain::{errors::DomainError, models::SortDirection};
use crate::{
dtos::{DiaryQueryParams, LogReviewForm},
errors::ApiError,
extractors::AuthenticatedUser,
state::AppState,
};
pub async fn get_diary_page(
State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>,
) -> Result<impl IntoResponse, ApiError> {
let query = GetDiaryQuery {
limit: params.limit,
offset: params.offset,
sort_by: params.sort_by.as_deref().map(|s| {
if s == "asc" {
SortDirection::Ascending
} else {
SortDirection::Descending
}
}),
movie_id: params.movie_id,
};
let page = get_diary::execute(&state.app_ctx, query).await?;
let html = state
.html_renderer
.render_diary_page(&page)
.map_err(|e| ApiError(DomainError::InfrastructureError(e)))?;
Ok(Html(html))
}
pub async fn post_review(
State(state): State<AppState>,
user: AuthenticatedUser,
Form(form): Form<LogReviewForm>,
) -> Result<impl IntoResponse, ApiError> {
let watched_at = NaiveDateTime::parse_from_str(&form.watched_at, "%Y-%m-%dT%H:%M:%S")
.map_err(|_| {
ApiError(DomainError::ValidationError(
"Invalid watched_at format, expected YYYY-MM-DDTHH:MM:SS".into(),
))
})?;
let cmd = LogReviewCommand {
external_metadata_id: form.external_metadata_id,
manual_title: form.manual_title,
manual_release_year: form.manual_release_year,
manual_director: form.manual_director,
user_id: user.0.value(),
rating: form.rating,
comment: form.comment,
watched_at,
};
log_review::execute(&state.app_ctx, cmd).await?;
Ok(Redirect::to("/diary"))
}
}
pub mod api {
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::NaiveDateTime;
use uuid::Uuid;
use application::{
commands::{LogReviewCommand, SyncPosterCommand},
queries::{GetDiaryQuery, GetReviewHistoryQuery},
use_cases::{get_diary, get_review_history, log_review, sync_poster},
};
use domain::{
errors::DomainError,
models::{DiaryEntry, Movie, Review, SortDirection},
services::review_history::Trend,
value_objects::MovieId,
};
use crate::{
dtos::{
DiaryEntryDto, DiaryQueryParams, DiaryResponse, LoginRequest, LoginResponse,
LogReviewRequest, MovieDto, ReviewDto, ReviewHistoryResponse,
},
errors::ApiError,
extractors::AuthenticatedUser,
state::AppState,
};
pub async fn get_diary(
State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> {
let query = GetDiaryQuery {
limit: params.limit,
offset: params.offset,
sort_by: params.sort_by.as_deref().map(|s| {
if s == "asc" {
SortDirection::Ascending
} else {
SortDirection::Descending
}
}),
movie_id: params.movie_id,
};
let page = get_diary::execute(&state.app_ctx, query).await?;
Ok(Json(DiaryResponse {
items: page.items.iter().map(entry_to_dto).collect(),
total_count: page.total_count,
limit: page.limit,
offset: page.offset,
}))
}
pub async fn get_review_history(
State(state): State<AppState>,
Path(movie_id): Path<Uuid>,
) -> Result<Json<ReviewHistoryResponse>, ApiError> {
let (history, trend) = get_review_history::execute(
&state.app_ctx,
GetReviewHistoryQuery { movie_id },
)
.await?;
Ok(Json(ReviewHistoryResponse {
movie: movie_to_dto(history.movie()),
viewings: history.viewings().iter().map(review_to_dto).collect(),
trend: match trend {
Trend::Improved => "improved",
Trend::Declined => "declined",
Trend::Neutral => "neutral",
}
.to_string(),
}))
}
pub async fn post_review(
State(state): State<AppState>,
user: AuthenticatedUser,
Json(req): Json<LogReviewRequest>,
) -> Result<impl IntoResponse, ApiError> {
let watched_at = NaiveDateTime::parse_from_str(&req.watched_at, "%Y-%m-%dT%H:%M:%S")
.map_err(|_| {
ApiError(DomainError::ValidationError(
"Invalid watched_at format, expected YYYY-MM-DDTHH:MM:SS".into(),
))
})?;
let cmd = LogReviewCommand {
external_metadata_id: req.external_metadata_id,
manual_title: req.manual_title,
manual_release_year: req.manual_release_year,
manual_director: req.manual_director,
user_id: user.0.value(),
rating: req.rating,
comment: req.comment,
watched_at,
};
log_review::execute(&state.app_ctx, cmd).await?;
Ok(StatusCode::CREATED)
}
pub async fn sync_poster(
State(state): State<AppState>,
_user: AuthenticatedUser,
Path(movie_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
let movie = state
.app_ctx
.repository
.get_movie_by_id(&MovieId::from_uuid(movie_id))
.await?
.ok_or_else(|| ApiError(DomainError::NotFound(format!("Movie {movie_id}"))))?;
let external_id = movie
.external_metadata_id()
.ok_or_else(|| {
ApiError(DomainError::ValidationError(
"Movie has no external metadata ID, cannot sync poster".into(),
))
})?
.value()
.to_string();
sync_poster::execute(
&state.app_ctx,
SyncPosterCommand {
movie_id,
external_metadata_id: external_id,
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn login(
State(_state): State<AppState>,
Json(_req): Json<LoginRequest>,
) -> Json<LoginResponse> {
Json(LoginResponse {
token: "stub-token".to_string(),
})
}
fn movie_to_dto(movie: &Movie) -> MovieDto {
MovieDto {
id: movie.id().value(),
title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
director: movie.director().map(|d| d.to_string()),
poster_path: movie.poster_path().map(|p| p.value().to_string()),
}
}
fn review_to_dto(review: &Review) -> ReviewDto {
ReviewDto {
id: review.id().value(),
rating: review.rating().value(),
comment: review.comment().map(|c| c.value().to_string()),
watched_at: review.watched_at().to_string(),
}
}
fn entry_to_dto(entry: &DiaryEntry) -> DiaryEntryDto {
DiaryEntryDto {
movie: movie_to_dto(entry.movie()),
review: review_to_dto(entry.review()),
}
}
}