refactor: extract view-model mappers from presentation handlers
Some checks failed
CI / Check / Test (push) Has been cancelled
Some checks failed
CI / Check / Test (push) Has been cancelled
Move mapping logic (domain→DTO/template structs) into mappers/ module. Handlers now call mapper fns instead of inline conversions.
This commit is contained in:
13
crates/presentation/src/mappers/diary.rs
Normal file
13
crates/presentation/src/mappers/diary.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use api_types::FeedEntryDto;
|
||||
use domain::models::FeedEntry;
|
||||
|
||||
use super::movies::{movie_to_dto, review_to_dto};
|
||||
|
||||
pub fn feed_entry_to_dto(e: &FeedEntry) -> FeedEntryDto {
|
||||
FeedEntryDto {
|
||||
movie: movie_to_dto(e.movie()),
|
||||
review: review_to_dto(e.review()),
|
||||
user_email: e.user_email().to_string(),
|
||||
user_display_name: e.user_display_name().to_string(),
|
||||
}
|
||||
}
|
||||
32
crates/presentation/src/mappers/import.rs
Normal file
32
crates/presentation/src/mappers/import.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use domain::models::AnnotatedRow;
|
||||
use domain::models::import::RowResult;
|
||||
use template_askama::{ImportPreviewRow, ImportRowStatus};
|
||||
|
||||
pub fn annotated_to_preview_row(idx: usize, annotated: &AnnotatedRow) -> ImportPreviewRow {
|
||||
match &annotated.result {
|
||||
RowResult::Valid(row) => {
|
||||
let cells = vec![
|
||||
row.title.clone().unwrap_or_default(),
|
||||
row.release_year.clone().unwrap_or_default(),
|
||||
row.director.clone().unwrap_or_default(),
|
||||
row.rating.clone().unwrap_or_default(),
|
||||
row.watched_at.clone().unwrap_or_default(),
|
||||
row.comment.clone().unwrap_or_default(),
|
||||
];
|
||||
ImportPreviewRow {
|
||||
index: idx,
|
||||
status: if annotated.is_duplicate {
|
||||
ImportRowStatus::Duplicate
|
||||
} else {
|
||||
ImportRowStatus::Valid
|
||||
},
|
||||
cells,
|
||||
}
|
||||
}
|
||||
RowResult::Invalid { errors, raw } => ImportPreviewRow {
|
||||
index: idx,
|
||||
status: ImportRowStatus::Invalid(errors.join("; ")),
|
||||
cells: raw.iter().map(|(_, v)| v.clone()).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
25
crates/presentation/src/mappers/integrations.rs
Normal file
25
crates/presentation/src/mappers/integrations.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use domain::models::{WatchEvent, WebhookToken};
|
||||
use template_askama::{WatchQueueDisplayEntry, WebhookTokenView};
|
||||
|
||||
pub fn webhook_token_view(t: &WebhookToken) -> WebhookTokenView {
|
||||
WebhookTokenView {
|
||||
id: t.id().value().to_string(),
|
||||
provider: t.provider().to_string(),
|
||||
label: t.label().map(String::from),
|
||||
created_at: t.created_at().format("%Y-%m-%d %H:%M").to_string(),
|
||||
last_used_at: t
|
||||
.last_used_at()
|
||||
.map(|d| d.format("%Y-%m-%d %H:%M").to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch_queue_entry(e: &WatchEvent) -> WatchQueueDisplayEntry {
|
||||
WatchQueueDisplayEntry {
|
||||
id: e.id().value().to_string(),
|
||||
title: e.title().to_string(),
|
||||
year: e.year(),
|
||||
source: e.source().to_string(),
|
||||
watched_at: e.watched_at().format("%Y-%m-%d %H:%M").to_string(),
|
||||
movie_url: e.movie_id().map(|m| format!("/movies/{}", m.value())),
|
||||
}
|
||||
}
|
||||
5
crates/presentation/src/mappers/mod.rs
Normal file
5
crates/presentation/src/mappers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod diary;
|
||||
pub mod import;
|
||||
pub mod integrations;
|
||||
pub mod movies;
|
||||
pub mod users;
|
||||
48
crates/presentation/src/mappers/movies.rs
Normal file
48
crates/presentation/src/mappers/movies.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use api_types::{DiaryEntryDto, MovieDto, ReviewDto};
|
||||
use domain::models::{DiaryEntry, Movie, MovieSummary, Review};
|
||||
|
||||
pub 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()),
|
||||
genres: vec![],
|
||||
runtime_minutes: None,
|
||||
original_language: None,
|
||||
overview: None,
|
||||
collection_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary_to_dto(summary: &MovieSummary) -> MovieDto {
|
||||
MovieDto {
|
||||
id: summary.movie.id().value(),
|
||||
title: summary.movie.title().value().to_string(),
|
||||
release_year: summary.movie.release_year().value(),
|
||||
director: summary.movie.director().map(|d| d.to_string()),
|
||||
poster_path: summary.movie.poster_path().map(|p| p.value().to_string()),
|
||||
genres: summary.genres.clone(),
|
||||
runtime_minutes: summary.runtime_minutes,
|
||||
original_language: summary.original_language.clone(),
|
||||
overview: summary.overview.clone(),
|
||||
collection_name: summary.collection_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entry_to_dto(entry: &DiaryEntry) -> DiaryEntryDto {
|
||||
DiaryEntryDto {
|
||||
movie: movie_to_dto(entry.movie()),
|
||||
review: review_to_dto(entry.review()),
|
||||
}
|
||||
}
|
||||
42
crates/presentation/src/mappers/users.rs
Normal file
42
crates/presentation/src/mappers/users.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use application::users::get_profile::PendingFollowerView;
|
||||
use domain::models::UserSummary;
|
||||
use domain::ports::RemoteActorInfo;
|
||||
use template_askama::{RemoteActorData, RemoteActorDisplay, UserSummaryView};
|
||||
|
||||
pub fn user_summary_view(u: &UserSummary) -> UserSummaryView {
|
||||
let name = u.email().split('@').next().unwrap_or("?").to_string();
|
||||
let initial = name.chars().next().unwrap_or('?').to_ascii_uppercase();
|
||||
let avg_display = u
|
||||
.avg_rating
|
||||
.map(|r| format!("{:.1}", r))
|
||||
.unwrap_or_else(|| "\u{2014}".to_string());
|
||||
let avatar_url = u.avatar_path.as_ref().map(|p| format!("/images/{}", p));
|
||||
UserSummaryView {
|
||||
user_id: u.user_id.value(),
|
||||
display_name: name,
|
||||
initial,
|
||||
avg_rating_display: avg_display,
|
||||
total_movies: u.total_movies,
|
||||
avatar_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote_actor_display(a: &RemoteActorInfo) -> RemoteActorDisplay {
|
||||
let display = a.display_name.clone().unwrap_or_else(|| a.handle.clone());
|
||||
let initial = display.chars().next().unwrap_or('?').to_ascii_uppercase();
|
||||
RemoteActorDisplay {
|
||||
handle: a.handle.clone(),
|
||||
display_name: display,
|
||||
initial,
|
||||
url: a.url.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pending_follower_data(p: &PendingFollowerView) -> RemoteActorData {
|
||||
RemoteActorData {
|
||||
handle: p.handle.clone(),
|
||||
url: p.url.clone(),
|
||||
display_name: p.display_name.clone(),
|
||||
avatar_url: p.avatar_url.clone(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user