refactor(diary): DeleteReviewDeps, GetMovieSocialPageDeps, GetActivityFeedDeps

This commit is contained in:
2026-06-11 22:37:35 +02:00
parent ddf100cfc2
commit 7bf5c47f5b
18 changed files with 238 additions and 119 deletions

View File

@@ -1,16 +1,15 @@
use crate::{context::AppContext, diary::commands::DeleteReviewCommand}; use crate::diary::{commands::DeleteReviewCommand, deps::DeleteReviewDeps};
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
events::DomainEvent, events::DomainEvent,
value_objects::{ReviewId, UserId}, value_objects::{ReviewId, UserId},
}; };
pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), DomainError> { pub async fn execute(deps: &DeleteReviewDeps, cmd: DeleteReviewCommand) -> Result<(), DomainError> {
let review_id = ReviewId::from_uuid(cmd.review_id); let review_id = ReviewId::from_uuid(cmd.review_id);
let requesting_user_id = UserId::from_uuid(cmd.requesting_user_id); let requesting_user_id = UserId::from_uuid(cmd.requesting_user_id);
let review = ctx let review = deps
.repos
.review .review
.get_review_by_id(&review_id) .get_review_by_id(&review_id)
.await? .await?
@@ -21,10 +20,9 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), D
} }
let movie_id = review.movie_id().clone(); let movie_id = review.movie_id().clone();
ctx.repos.review.delete_review(&review_id).await?; deps.review.delete_review(&review_id).await?;
if let Err(e) = ctx if let Err(e) = deps
.services
.event_publisher .event_publisher
.publish(&DomainEvent::ReviewDeleted { .publish(&DomainEvent::ReviewDeleted {
review_id: review_id.clone(), review_id: review_id.clone(),
@@ -35,13 +33,12 @@ pub async fn execute(ctx: &AppContext, cmd: DeleteReviewCommand) -> Result<(), D
tracing::warn!("failed to publish ReviewDeleted: {e}"); tracing::warn!("failed to publish ReviewDeleted: {e}");
} }
let history = ctx.repos.diary.get_review_history(&movie_id).await?; let history = deps.diary.get_review_history(&movie_id).await?;
if history.viewings().is_empty() { if history.viewings().is_empty() {
let poster_path = history.movie().poster_path().cloned(); let poster_path = history.movie().poster_path().cloned();
ctx.repos.movie.delete_movie(&movie_id).await?; deps.movie.delete_movie(&movie_id).await?;
// best-effort: movie is already deleted, so publish failure is non-fatal // best-effort: movie is already deleted, so publish failure is non-fatal
if let Err(e) = ctx if let Err(e) = deps
.services
.event_publisher .event_publisher
.publish(&DomainEvent::MovieDeleted { .publish(&DomainEvent::MovieDeleted {
movie_id, movie_id,

View File

@@ -0,0 +1,27 @@
use std::sync::Arc;
use domain::ports::{
DiaryRepository, EventPublisher, MovieProfileRepository, MovieRepository, ReviewRepository,
SocialQueryPort,
};
use crate::config::AppConfig;
pub struct DeleteReviewDeps {
pub review: Arc<dyn ReviewRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub movie: Arc<dyn MovieRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
}
pub struct GetMovieSocialPageDeps {
pub movie: Arc<dyn MovieRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
}
pub struct GetActivityFeedDeps {
pub diary: Arc<dyn DiaryRepository>,
pub social_query: Arc<dyn SocialQueryPort>,
pub config: AppConfig,
}

View File

@@ -1,15 +1,20 @@
use domain::{errors::DomainError, value_objects::UserId}; use std::sync::Arc;
use crate::{context::AppContext, diary::queries::ExportQuery}; use domain::{
errors::DomainError,
ports::{DiaryExporter, DiaryRepository},
value_objects::UserId,
};
pub async fn execute(ctx: &AppContext, query: ExportQuery) -> Result<Vec<u8>, DomainError> { use crate::diary::queries::ExportQuery;
let entries = ctx
.repos pub async fn execute(
.diary diary: &Arc<dyn DiaryRepository>,
diary_exporter: &Arc<dyn DiaryExporter>,
query: ExportQuery,
) -> Result<Vec<u8>, DomainError> {
let entries = diary
.get_user_history(&UserId::from_uuid(query.user_id)) .get_user_history(&UserId::from_uuid(query.user_id))
.await?; .await?;
ctx.services diary_exporter.serialize_entries(&entries, query.format).await
.diary_exporter
.serialize_entries(&entries, query.format)
.await
} }

View File

@@ -1,4 +1,4 @@
use crate::{context::AppContext, diary::queries::GetActivityFeedQuery}; use crate::diary::{deps::GetActivityFeedDeps, queries::GetActivityFeedQuery};
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
@@ -9,15 +9,14 @@ use domain::{
}; };
pub async fn execute( pub async fn execute(
ctx: &AppContext, deps: &GetActivityFeedDeps,
query: GetActivityFeedQuery, query: GetActivityFeedQuery,
) -> Result<Paginated<FeedEntry>, DomainError> { ) -> Result<Paginated<FeedEntry>, DomainError> {
let page = PageParams::new(Some(query.limit), Some(query.offset))?; let page = PageParams::new(Some(query.limit), Some(query.offset))?;
let following = build_following_filter(ctx, &query).await; let following = build_following_filter(deps, &query).await;
ctx.repos deps.diary
.diary
.query_activity_feed_filtered( .query_activity_feed_filtered(
&page, &page,
&query.sort_by, &query.sort_by,
@@ -28,15 +27,14 @@ pub async fn execute(
} }
async fn build_following_filter( async fn build_following_filter(
ctx: &AppContext, deps: &GetActivityFeedDeps,
query: &GetActivityFeedQuery, query: &GetActivityFeedQuery,
) -> Option<FollowingFilter> { ) -> Option<FollowingFilter> {
if !query.filter_following { if !query.filter_following {
return None; return None;
} }
let viewer_id = query.viewer_user_id?; let viewer_id = query.viewer_user_id?;
let urls = ctx let urls = deps
.repos
.social_query .social_query
.get_accepted_following_urls(viewer_id) .get_accepted_following_urls(viewer_id)
.await .await
@@ -47,7 +45,7 @@ async fn build_following_filter(
remote_actor_urls: vec![], remote_actor_urls: vec![],
}); });
} }
let base_url = &ctx.config.base_url; let base_url = &deps.config.base_url;
let mut local_ids = vec![viewer_id]; let mut local_ids = vec![viewer_id];
let mut remote_urls = Vec::new(); let mut remote_urls = Vec::new();
for url in urls { for url in urls {

View File

@@ -1,16 +1,19 @@
use std::sync::Arc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::{ models::{
DiaryEntry, DiaryFilter, SortDirection, DiaryEntry, DiaryFilter, SortDirection,
collections::{PageParams, Paginated}, collections::{PageParams, Paginated},
}, },
ports::DiaryRepository,
value_objects::{MovieId, UserId}, value_objects::{MovieId, UserId},
}; };
use crate::{context::AppContext, diary::queries::GetDiaryQuery}; use crate::diary::queries::GetDiaryQuery;
pub async fn execute( pub async fn execute(
ctx: &AppContext, diary: &Arc<dyn DiaryRepository>,
query: GetDiaryQuery, query: GetDiaryQuery,
) -> Result<Paginated<DiaryEntry>, DomainError> { ) -> Result<Paginated<DiaryEntry>, DomainError> {
let page = PageParams::new(query.limit, query.offset)?; let page = PageParams::new(query.limit, query.offset)?;
@@ -25,7 +28,7 @@ pub async fn execute(
search: None, search: None,
}; };
ctx.repos.diary.query_diary(&filter).await diary.query_diary(&filter).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -7,7 +7,7 @@ use domain::{
value_objects::MovieId, value_objects::MovieId,
}; };
use crate::{context::AppContext, diary::queries::GetMovieSocialPageQuery}; use crate::diary::{deps::GetMovieSocialPageDeps, queries::GetMovieSocialPageQuery};
pub struct MovieSocialPageResult { pub struct MovieSocialPageResult {
pub movie: Movie, pub movie: Movie,
@@ -17,23 +17,22 @@ pub struct MovieSocialPageResult {
} }
pub async fn execute( pub async fn execute(
ctx: &AppContext, deps: &GetMovieSocialPageDeps,
query: GetMovieSocialPageQuery, query: GetMovieSocialPageQuery,
) -> Result<MovieSocialPageResult, DomainError> { ) -> Result<MovieSocialPageResult, DomainError> {
let movie_id = MovieId::from_uuid(query.movie_id); let movie_id = MovieId::from_uuid(query.movie_id);
let page = PageParams::new(Some(query.limit), Some(query.offset))?; let page = PageParams::new(Some(query.limit), Some(query.offset))?;
let movie = ctx let movie = deps
.repos
.movie .movie
.get_movie_by_id(&movie_id) .get_movie_by_id(&movie_id)
.await? .await?
.ok_or_else(|| DomainError::NotFound(format!("Movie {}", query.movie_id)))?; .ok_or_else(|| DomainError::NotFound(format!("Movie {}", query.movie_id)))?;
let (stats, reviews, profile) = tokio::try_join!( let (stats, reviews, profile) = tokio::try_join!(
ctx.repos.diary.get_movie_stats(&movie_id), deps.diary.get_movie_stats(&movie_id),
ctx.repos.diary.get_movie_social_feed(&movie_id, &page), deps.diary.get_movie_social_feed(&movie_id, &page),
ctx.repos.movie_profile.get_by_movie_id(&movie_id), deps.movie_profile.get_by_movie_id(&movie_id),
)?; )?;
Ok(MovieSocialPageResult { Ok(MovieSocialPageResult {

View File

@@ -1,19 +1,22 @@
use std::sync::Arc;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
models::ReviewHistory, models::ReviewHistory,
ports::DiaryRepository,
services::review_history::{ReviewHistoryAnalyzer, Trend}, services::review_history::{ReviewHistoryAnalyzer, Trend},
value_objects::MovieId, value_objects::MovieId,
}; };
use crate::{context::AppContext, diary::queries::GetReviewHistoryQuery}; use crate::diary::queries::GetReviewHistoryQuery;
pub async fn execute( pub async fn execute(
ctx: &AppContext, diary: &Arc<dyn DiaryRepository>,
query: GetReviewHistoryQuery, query: GetReviewHistoryQuery,
) -> Result<(ReviewHistory, Trend), DomainError> { ) -> Result<(ReviewHistory, Trend), DomainError> {
let movie_id = MovieId::from_uuid(query.movie_id); let movie_id = MovieId::from_uuid(query.movie_id);
let mut history = ctx.repos.diary.get_review_history(&movie_id).await?; let mut history = diary.get_review_history(&movie_id).await?;
let trend = ReviewHistoryAnalyzer::rating_trend(&history)?; let trend = ReviewHistoryAnalyzer::rating_trend(&history)?;

View File

@@ -1,9 +1,14 @@
use std::sync::Arc;
use domain::errors::DomainError; use domain::errors::DomainError;
use crate::{context::AppContext, diary::commands::LogReviewCommand}; use crate::{diary::commands::LogReviewCommand, ports::ReviewLogger};
pub async fn execute(ctx: &AppContext, cmd: LogReviewCommand) -> Result<(), DomainError> { pub async fn execute(
ctx.services.review_logger.log_review(cmd).await review_logger: &Arc<dyn ReviewLogger>,
cmd: LogReviewCommand,
) -> Result<(), DomainError> {
review_logger.log_review(cmd).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,5 +1,6 @@
pub mod commands; pub mod commands;
pub mod delete_review; pub mod delete_review;
pub mod deps;
pub mod export_diary; pub mod export_diary;
pub mod get_activity_feed; pub mod get_activity_feed;
pub mod get_diary; pub mod get_diary;

View File

@@ -12,7 +12,9 @@ use domain::{
}; };
use crate::{ use crate::{
diary::commands::DeleteReviewCommand, diary::delete_review, test_helpers::TestContextBuilder, diary::commands::DeleteReviewCommand,
diary::delete_review,
diary::deps::DeleteReviewDeps,
}; };
fn make_movie() -> Movie { fn make_movie() -> Movie {
@@ -51,15 +53,15 @@ async fn test_delete_review_removes_it() {
reviews.save_review(&review).await.unwrap(); reviews.save_review(&review).await.unwrap();
diary.seed_history(movie.clone(), vec![]); diary.seed_history(movie.clone(), vec![]);
let ctx = TestContextBuilder::new() let deps = DeleteReviewDeps {
.with_movies(Arc::clone(&movies) as _) review: Arc::clone(&reviews) as _,
.with_reviews(Arc::clone(&reviews) as _) diary: diary.clone() as _,
.with_diary(Arc::clone(&diary) as _) movie: Arc::clone(&movies) as _,
.with_event_publisher(Arc::clone(&events) as _) event_publisher: Arc::clone(&events) as _,
.build(); };
delete_review::execute( delete_review::execute(
&ctx, &deps,
DeleteReviewCommand { DeleteReviewCommand {
review_id: review.id().value(), review_id: review.id().value(),
requesting_user_id: user_id.value(), requesting_user_id: user_id.value(),
@@ -78,6 +80,9 @@ async fn test_delete_review_removes_it() {
#[tokio::test] #[tokio::test]
async fn test_delete_review_wrong_user_is_unauthorized() { async fn test_delete_review_wrong_user_is_unauthorized() {
let reviews = InMemoryReviewRepository::new(); let reviews = InMemoryReviewRepository::new();
let diary = FakeDiaryRepository::new();
let movies = InMemoryMovieRepository::new();
let events = NoopEventPublisher::new();
let movie_id = MovieId::from_uuid(uuid::Uuid::new_v4()); let movie_id = MovieId::from_uuid(uuid::Uuid::new_v4());
let owner_id = UserId::from_uuid(uuid::Uuid::new_v4()); let owner_id = UserId::from_uuid(uuid::Uuid::new_v4());
@@ -86,12 +91,15 @@ async fn test_delete_review_wrong_user_is_unauthorized() {
reviews.save_review(&review).await.unwrap(); reviews.save_review(&review).await.unwrap();
let ctx = TestContextBuilder::new() let deps = DeleteReviewDeps {
.with_reviews(Arc::clone(&reviews) as _) review: Arc::clone(&reviews) as _,
.build(); diary: diary as _,
movie: movies as _,
event_publisher: Arc::clone(&events) as _,
};
let result = delete_review::execute( let result = delete_review::execute(
&ctx, &deps,
DeleteReviewCommand { DeleteReviewCommand {
review_id: review.id().value(), review_id: review.id().value(),
requesting_user_id: other_id, requesting_user_id: other_id,

View File

@@ -2,18 +2,30 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use domain::errors::DomainError; use domain::errors::DomainError;
use domain::testing::{FakeDiaryRepository, NoopSocialQueryPort};
use crate::{ use crate::{
diary::get_activity_feed, diary::queries::GetActivityFeedQuery, config::AppConfig,
diary::deps::GetActivityFeedDeps,
diary::get_activity_feed,
diary::queries::GetActivityFeedQuery,
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
}; };
fn default_deps() -> GetActivityFeedDeps {
GetActivityFeedDeps {
diary: FakeDiaryRepository::new() as _,
social_query: Arc::new(NoopSocialQueryPort),
config: TestContextBuilder::new().config,
}
}
#[tokio::test] #[tokio::test]
async fn returns_empty_feed() { async fn returns_empty_feed() {
let ctx = TestContextBuilder::new().build(); let deps = default_deps();
let result = get_activity_feed::execute( let result = get_activity_feed::execute(
&ctx, &deps,
GetActivityFeedQuery { GetActivityFeedQuery {
limit: 10, limit: 10,
offset: 0, offset: 0,
@@ -32,12 +44,12 @@ async fn returns_empty_feed() {
#[tokio::test] #[tokio::test]
async fn returns_feed_with_following_filter() { async fn returns_feed_with_following_filter() {
let ctx = TestContextBuilder::new().build(); let deps = default_deps();
let viewer = uuid::Uuid::new_v4(); let viewer = uuid::Uuid::new_v4();
let result = get_activity_feed::execute( let result = get_activity_feed::execute(
&ctx, &deps,
GetActivityFeedQuery { GetActivityFeedQuery {
limit: 10, limit: 10,
offset: 0, offset: 0,
@@ -93,12 +105,24 @@ async fn following_filter_parses_local_and_remote_urls() {
let social = Arc::new(FakeSocialWithFollowing(following_urls)); let social = Arc::new(FakeSocialWithFollowing(following_urls));
let ctx = TestContextBuilder::new() let deps = GetActivityFeedDeps {
.with_social_query(social as _) diary: FakeDiaryRepository::new() as _,
.build(); social_query: social as _,
config: AppConfig {
allow_registration: true,
base_url: "http://localhost:3000".into(),
rate_limit: 20,
refresh_ttl_seconds: 2_592_000,
wrapup: crate::config::WrapUpConfig {
font_path: None,
logo_path: None,
bg_dir: None,
},
},
};
let result = get_activity_feed::execute( let result = get_activity_feed::execute(
&ctx, &deps,
GetActivityFeedQuery { GetActivityFeedQuery {
limit: 10, limit: 10,
offset: 0, offset: 0,
@@ -118,10 +142,10 @@ async fn following_filter_parses_local_and_remote_urls() {
#[tokio::test] #[tokio::test]
async fn following_filter_without_viewer_returns_none() { async fn following_filter_without_viewer_returns_none() {
let ctx = TestContextBuilder::new().build(); let deps = default_deps();
let result = get_activity_feed::execute( let result = get_activity_feed::execute(
&ctx, &deps,
GetActivityFeedQuery { GetActivityFeedQuery {
limit: 10, limit: 10,
offset: 0, offset: 0,

View File

@@ -1,11 +1,14 @@
use crate::{diary::get_diary, diary::queries::GetDiaryQuery, test_helpers::TestContextBuilder}; use domain::testing::FakeDiaryRepository;
use std::sync::Arc;
use crate::{diary::get_diary, diary::queries::GetDiaryQuery};
#[tokio::test] #[tokio::test]
async fn returns_empty_page() { async fn returns_empty_page() {
let ctx = TestContextBuilder::new().build(); let diary = FakeDiaryRepository::new() as Arc<dyn domain::ports::DiaryRepository>;
let result = get_diary::execute( let result = get_diary::execute(
&ctx, &diary,
GetDiaryQuery { GetDiaryQuery {
limit: None, limit: None,
offset: None, offset: None,

View File

@@ -5,21 +5,26 @@ use uuid::Uuid;
use domain::{ use domain::{
models::Movie, models::Movie,
ports::MovieRepository, ports::MovieRepository,
testing::InMemoryMovieRepository, testing::{FakeDiaryRepository, InMemoryMovieProfileRepository, InMemoryMovieRepository},
value_objects::{MovieTitle, ReleaseYear}, value_objects::{MovieTitle, ReleaseYear},
}; };
use crate::{ use crate::{
diary::get_movie_social_page, diary::queries::GetMovieSocialPageQuery, diary::deps::GetMovieSocialPageDeps,
test_helpers::TestContextBuilder, diary::get_movie_social_page,
diary::queries::GetMovieSocialPageQuery,
}; };
#[tokio::test] #[tokio::test]
async fn fails_when_movie_not_found() { async fn fails_when_movie_not_found() {
let ctx = TestContextBuilder::new().build(); let deps = GetMovieSocialPageDeps {
movie: InMemoryMovieRepository::new(),
diary: FakeDiaryRepository::new() as _,
movie_profile: InMemoryMovieProfileRepository::new(),
};
let result = get_movie_social_page::execute( let result = get_movie_social_page::execute(
&ctx, &deps,
GetMovieSocialPageQuery { GetMovieSocialPageQuery {
movie_id: Uuid::new_v4(), movie_id: Uuid::new_v4(),
limit: 10, limit: 10,
@@ -45,12 +50,14 @@ async fn returns_movie_social_page() {
let movie_uuid = movie.id().value(); let movie_uuid = movie.id().value();
movies.upsert_movie(&movie).await.unwrap(); movies.upsert_movie(&movie).await.unwrap();
let ctx = TestContextBuilder::new() let deps = GetMovieSocialPageDeps {
.with_movies(Arc::clone(&movies) as _) movie: Arc::clone(&movies) as _,
.build(); diary: FakeDiaryRepository::new() as _,
movie_profile: InMemoryMovieProfileRepository::new(),
};
let result = get_movie_social_page::execute( let result = get_movie_social_page::execute(
&ctx, &deps,
GetMovieSocialPageQuery { GetMovieSocialPageQuery {
movie_id: movie_uuid, movie_id: movie_uuid,
limit: 10, limit: 10,

View File

@@ -1,13 +1,13 @@
use std::sync::Arc;
use domain::{ use domain::{
models::Movie, models::Movie,
ports::DiaryRepository,
services::review_history::Trend, services::review_history::Trend,
value_objects::{MovieTitle, ReleaseYear}, value_objects::{MovieTitle, ReleaseYear},
}; };
use crate::{ use crate::{diary::get_review_history, diary::queries::GetReviewHistoryQuery};
diary::get_review_history, diary::queries::GetReviewHistoryQuery,
test_helpers::TestContextBuilder,
};
#[tokio::test] #[tokio::test]
async fn returns_empty_history() { async fn returns_empty_history() {
@@ -22,10 +22,9 @@ async fn returns_empty_history() {
let diary = domain::testing::FakeDiaryRepository::new(); let diary = domain::testing::FakeDiaryRepository::new();
diary.seed_history(movie, vec![]); diary.seed_history(movie, vec![]);
let diary: Arc<dyn DiaryRepository> = diary;
let ctx = TestContextBuilder::new().with_diary(diary as _).build(); let (history, trend) = get_review_history::execute(&diary, GetReviewHistoryQuery { movie_id })
let (history, trend) = get_review_history::execute(&ctx, GetReviewHistoryQuery { movie_id })
.await .await
.unwrap(); .unwrap();

View File

@@ -17,24 +17,18 @@ use crate::{
test_helpers::TestContextBuilder, test_helpers::TestContextBuilder,
}; };
fn build_ctx_with_real_logger( fn build_logger(
movies: &Arc<InMemoryMovieRepository>, movies: &Arc<InMemoryMovieRepository>,
reviews: &Arc<InMemoryReviewRepository>, reviews: &Arc<InMemoryReviewRepository>,
events: &Arc<NoopEventPublisher>, events: &Arc<NoopEventPublisher>,
) -> crate::context::AppContext { ) -> Arc<dyn crate::ports::ReviewLogger> {
let logger = Arc::new(DefaultReviewLogger::new( Arc::new(DefaultReviewLogger::new(
Arc::clone(movies) as _, Arc::clone(movies) as _,
Arc::clone(reviews) as _, Arc::clone(reviews) as _,
crate::test_helpers::TestContextBuilder::new().watchlist_repo, TestContextBuilder::new().watchlist_repo,
Arc::new(domain::testing::FakeMetadataClient) as _, Arc::new(domain::testing::FakeMetadataClient) as _,
Arc::clone(events) as _, Arc::clone(events) as _,
)); ))
TestContextBuilder::new()
.with_movies(Arc::clone(movies) as _)
.with_reviews(Arc::clone(reviews) as _)
.with_event_publisher(Arc::clone(events) as _)
.with_review_logger(logger)
.build()
} }
fn movie_input_manual(title: &str, year: u16) -> MovieInput { fn movie_input_manual(title: &str, year: u16) -> MovieInput {
@@ -62,7 +56,7 @@ async fn test_log_review_creates_movie_and_review() {
let movies = InMemoryMovieRepository::new(); let movies = InMemoryMovieRepository::new();
let reviews = InMemoryReviewRepository::new(); let reviews = InMemoryReviewRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = build_ctx_with_real_logger(&movies, &reviews, &events); let logger = build_logger(&movies, &reviews, &events);
let user_id = uuid::Uuid::new_v4(); let user_id = uuid::Uuid::new_v4();
let cmd = LogReviewCommand { let cmd = LogReviewCommand {
@@ -73,7 +67,7 @@ async fn test_log_review_creates_movie_and_review() {
watched_at: Utc::now().naive_utc(), watched_at: Utc::now().naive_utc(),
}; };
log_review::execute(&ctx, cmd).await.unwrap(); log_review::execute(&logger, cmd).await.unwrap();
assert_eq!(reviews.count(), 1, "review should be saved"); assert_eq!(reviews.count(), 1, "review should be saved");
assert!(!events.published().is_empty(), "events should be published"); assert!(!events.published().is_empty(), "events should be published");
@@ -95,7 +89,7 @@ async fn test_log_review_reuses_existing_movie() {
movies.upsert_movie(&existing_movie).await.unwrap(); movies.upsert_movie(&existing_movie).await.unwrap();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = build_ctx_with_real_logger(&movies, &reviews, &events); let logger = build_logger(&movies, &reviews, &events);
let cmd = LogReviewCommand { let cmd = LogReviewCommand {
user_id: uuid::Uuid::new_v4(), user_id: uuid::Uuid::new_v4(),
@@ -105,7 +99,7 @@ async fn test_log_review_reuses_existing_movie() {
watched_at: Utc::now().naive_utc(), watched_at: Utc::now().naive_utc(),
}; };
log_review::execute(&ctx, cmd).await.unwrap(); log_review::execute(&logger, cmd).await.unwrap();
assert_eq!(movies.count(), 1, "no duplicate movie"); assert_eq!(movies.count(), 1, "no duplicate movie");
assert_eq!(reviews.count(), 1); assert_eq!(reviews.count(), 1);
@@ -116,7 +110,8 @@ async fn test_log_review_with_invalid_rating_fails() {
let movies = InMemoryMovieRepository::new(); let movies = InMemoryMovieRepository::new();
let reviews = InMemoryReviewRepository::new(); let reviews = InMemoryReviewRepository::new();
let events = NoopEventPublisher::new(); let events = NoopEventPublisher::new();
let ctx = build_ctx_with_real_logger(&movies, &reviews, &events); let logger = build_logger(&movies, &reviews, &events);
let cmd = LogReviewCommand { let cmd = LogReviewCommand {
user_id: uuid::Uuid::new_v4(), user_id: uuid::Uuid::new_v4(),
input: movie_input_manual("Some Film", 2000), input: movie_input_manual("Some Film", 2000),
@@ -124,6 +119,6 @@ async fn test_log_review_with_invalid_rating_fails() {
comment: None, comment: None,
watched_at: Utc::now().naive_utc(), watched_at: Utc::now().naive_utc(),
}; };
let result = log_review::execute(&ctx, cmd).await; let result = log_review::execute(&logger, cmd).await;
assert!(result.is_err(), "rating > 5 should fail"); assert!(result.is_err(), "rating > 5 should fail");
} }

View File

@@ -11,6 +11,7 @@ use application::diary::{
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc, get_diary, delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc, get_diary,
log_review, log_review,
queries::{ExportQuery, GetActivityFeedQuery}, queries::{ExportQuery, GetActivityFeedQuery},
deps::{DeleteReviewDeps, GetActivityFeedDeps},
}; };
use domain::models::ExportFormat; use domain::models::ExportFormat;
@@ -50,7 +51,7 @@ pub async fn get_diary(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<DiaryQueryParams>, Query(params): Query<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> { ) -> Result<Json<DiaryResponse>, ApiError> {
let page = get_diary::execute(&state.app_ctx, to_diary_query(params)).await?; let page = get_diary::execute(&state.app_ctx.repos.diary, to_diary_query(params)).await?;
Ok(Json(DiaryResponse { Ok(Json(DiaryResponse {
items: page items: page
@@ -80,7 +81,7 @@ pub async fn post_review(
Json(req): Json<LogReviewRequest>, Json(req): Json<LogReviewRequest>,
) -> Result<impl IntoResponse, ApiError> { ) -> Result<impl IntoResponse, ApiError> {
let data = LogReviewData::try_from(req).map_err(ApiError)?; let data = LogReviewData::try_from(req).map_err(ApiError)?;
log_review::execute(&state.app_ctx, data.into_command(user.0.value())).await?; log_review::execute(&state.app_ctx.services.review_logger, data.into_command(user.0.value())).await?;
Ok(StatusCode::CREATED) Ok(StatusCode::CREATED)
} }
@@ -104,7 +105,13 @@ pub async fn delete_review(
review_id, review_id,
requesting_user_id: user_id.value(), requesting_user_id: user_id.value(),
}; };
delete_review::execute(&state.app_ctx, cmd).await?; let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie: state.app_ctx.repos.movie.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
delete_review::execute(&deps, cmd).await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -136,7 +143,13 @@ pub async fn export_diary(
user_id: user.0.value(), user_id: user.0.value(),
format, format,
}; };
match export_diary_uc::execute(&state.app_ctx, query).await { match export_diary_uc::execute(
&state.app_ctx.repos.diary,
&state.app_ctx.services.diary_exporter,
query,
)
.await
{
Ok(bytes) => ( Ok(bytes) => (
StatusCode::OK, StatusCode::OK,
[ [
@@ -165,8 +178,13 @@ pub async fn get_activity_feed(
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<ActivityFeedQueryParams>, Query(params): Query<ActivityFeedQueryParams>,
) -> Result<Json<ActivityFeedResponse>, ApiError> { ) -> Result<Json<ActivityFeedResponse>, ApiError> {
let deps = GetActivityFeedDeps {
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query.clone(),
config: state.app_ctx.config.clone(),
};
let page = get_feed_uc::execute( let page = get_feed_uc::execute(
&state.app_ctx, &deps,
GetActivityFeedQuery { GetActivityFeedQuery {
limit: params.limit.unwrap_or(20), limit: params.limit.unwrap_or(20),
offset: params.offset.unwrap_or(0), offset: params.offset.unwrap_or(0),
@@ -226,7 +244,7 @@ pub async fn post_review_html(
} }
}; };
match log_review::execute(&state.app_ctx, data.into_command(user_id.value())).await { match log_review::execute(&state.app_ctx.services.review_logger, data.into_command(user_id.value())).await {
Ok(_) => Redirect::to("/").into_response(), Ok(_) => Redirect::to("/").into_response(),
Err(e) => { Err(e) => {
let msg = encode_error(&e.to_string()); let msg = encode_error(&e.to_string());
@@ -249,7 +267,13 @@ pub async fn post_delete_review_html(
review_id, review_id,
requesting_user_id: user_id.value(), requesting_user_id: user_id.value(),
}; };
match delete_review::execute(&state.app_ctx, cmd).await { let deps = DeleteReviewDeps {
review: state.app_ctx.repos.review.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie: state.app_ctx.repos.movie.clone(),
event_publisher: state.app_ctx.services.event_publisher.clone(),
};
match delete_review::execute(&deps, cmd).await {
Ok(()) => { Ok(()) => {
let redirect_url = form let redirect_url = form
.redirect_after .redirect_after
@@ -281,7 +305,13 @@ pub async fn get_export_html(
user_id: user_id.value(), user_id: user_id.value(),
format, format,
}; };
match export_diary_uc::execute(&state.app_ctx, query).await { match export_diary_uc::execute(
&state.app_ctx.repos.diary,
&state.app_ctx.services.diary_exporter,
query,
)
.await
{
Ok(bytes) => ( Ok(bytes) => (
StatusCode::OK, StatusCode::OK,
[ [
@@ -332,7 +362,13 @@ pub async fn get_activity_feed_html(
filter_following, filter_following,
}; };
match application::diary::get_activity_feed::execute(&state.app_ctx, query).await { let deps = GetActivityFeedDeps {
diary: state.app_ctx.repos.diary.clone(),
social_query: state.app_ctx.repos.social_query.clone(),
config: state.app_ctx.config.clone(),
};
match application::diary::get_activity_feed::execute(&deps, query).await {
Ok(entries) => { Ok(entries) => {
let entry_limit = entries.limit; let entry_limit = entries.limit;
let entry_offset = entries.offset; let entry_offset = entries.offset;

View File

@@ -9,6 +9,7 @@ use uuid::Uuid;
use application::{ use application::{
diary::{ diary::{
commands::SyncPosterCommand, commands::SyncPosterCommand,
deps::{GetMovieSocialPageDeps},
get_movie_social_page, get_review_history, get_movie_social_page, get_review_history,
queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery}, queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery},
}, },
@@ -88,7 +89,7 @@ pub async fn get_review_history(
Path(movie_id): Path<Uuid>, Path(movie_id): Path<Uuid>,
) -> Result<Json<ReviewHistoryResponse>, ApiError> { ) -> Result<Json<ReviewHistoryResponse>, ApiError> {
let (history, trend) = let (history, trend) =
get_review_history::execute(&state.app_ctx, GetReviewHistoryQuery { movie_id }).await?; get_review_history::execute(&state.app_ctx.repos.diary, GetReviewHistoryQuery { movie_id }).await?;
Ok(Json(ReviewHistoryResponse { Ok(Json(ReviewHistoryResponse {
movie: crate::mappers::movies::movie_to_dto(history.movie()), movie: crate::mappers::movies::movie_to_dto(history.movie()),
@@ -154,7 +155,11 @@ pub async fn get_movie_detail(
let offset = params.offset.unwrap_or(0); let offset = params.offset.unwrap_or(0);
let result = get_movie_social_page::execute( let result = get_movie_social_page::execute(
&state.app_ctx, &GetMovieSocialPageDeps {
movie: state.app_ctx.repos.movie.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},
GetMovieSocialPageQuery { GetMovieSocialPageQuery {
movie_id, movie_id,
limit, limit,
@@ -288,7 +293,11 @@ pub async fn get_movie_detail_html(
let offset = params.offset.unwrap_or(0); let offset = params.offset.unwrap_or(0);
match get_movie_social_page::execute( match get_movie_social_page::execute(
&state.app_ctx, &GetMovieSocialPageDeps {
movie: state.app_ctx.repos.movie.clone(),
diary: state.app_ctx.repos.diary.clone(),
movie_profile: state.app_ctx.repos.movie_profile.clone(),
},
GetMovieSocialPageQuery { GetMovieSocialPageQuery {
movie_id, movie_id,
limit, limit,

View File

@@ -18,7 +18,7 @@ pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse
movie_id: None, movie_id: None,
user_id: None, user_id: None,
}; };
let page = get_diary::execute(&state.app_ctx, query).await?; let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
let xml = state let xml = state
.rss_renderer .rss_renderer
.render_feed(&page.items, "Movie Diary") .render_feed(&page.items, "Movie Diary")
@@ -49,7 +49,7 @@ pub async fn get_user_feed(
movie_id: None, movie_id: None,
user_id: Some(user_id), user_id: Some(user_id),
}; };
let page = get_diary::execute(&state.app_ctx, query).await?; let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
let display_name = user.email().value().split('@').next().unwrap_or("User"); let display_name = user.email().value().split('@').next().unwrap_or("User");
let title = format!("{}'s Movie Diary", display_name); let title = format!("{}'s Movie Diary", display_name);