structural refactor and codebase improvements

This commit is contained in:
2026-08-09 14:58:14 +02:00
parent 22b1dd3f56
commit c9715baab8
247 changed files with 11515 additions and 3063 deletions

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use domain::ports::{
DiaryQuery, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery, ReviewRepository,
SocialQuery,
DiaryExporter, DiaryQuery, EventPublisher, FollowGraphQuery, MovieCommand,
MovieProfileRepository, MovieQuery, ReviewRepository, UserRepository,
};
use crate::config::AppConfig;
@@ -27,6 +27,24 @@ pub struct GetMovieSocialPageDeps {
pub struct GetActivityFeedDeps {
pub diary: Arc<dyn DiaryQuery>,
pub social_query: Arc<dyn SocialQuery>,
pub social_query: Arc<dyn FollowGraphQuery>,
pub config: AppConfig,
}
pub struct GetUserFeedDeps {
pub user: Arc<dyn UserRepository>,
pub diary: Arc<dyn DiaryQuery>,
}
pub struct GetDiaryDeps {
pub diary: Arc<dyn DiaryQuery>,
}
pub struct GetReviewHistoryDeps {
pub diary: Arc<dyn DiaryQuery>,
}
pub struct ExportDiaryDeps {
pub diary: Arc<dyn DiaryQuery>,
pub diary_exporter: Arc<dyn DiaryExporter>,
}

View File

@@ -1,21 +1,16 @@
use std::sync::Arc;
use bytes::Bytes;
use domain::{
errors::DomainError,
ports::{DiaryExporter, DiaryQuery},
value_objects::UserId,
};
use domain::{errors::DomainError, value_objects::UserId};
use futures::stream::BoxStream;
use crate::diary::deps::ExportDiaryDeps;
use crate::diary::queries::ExportQuery;
pub fn execute(
diary: &Arc<dyn DiaryQuery>,
diary_exporter: &Arc<dyn DiaryExporter>,
deps: &ExportDiaryDeps,
query: ExportQuery,
) -> BoxStream<'static, Result<Bytes, DomainError>> {
let user_id = UserId::from_uuid(query.user_id);
let entry_stream = diary.stream_user_history(user_id);
diary_exporter.stream_entries(entry_stream, query.format)
let entry_stream = deps.diary.stream_user_history(user_id);
deps.diary_exporter
.stream_entries(entry_stream, query.format)
}

View File

@@ -1,19 +1,17 @@
use std::sync::Arc;
use domain::{
errors::DomainError,
models::{
DiaryEntry, DiaryFilter, ReviewSortBy,
collections::{PageParams, Paginated},
},
ports::DiaryQuery,
value_objects::{MovieId, UserId},
};
use crate::diary::deps::GetDiaryDeps;
use crate::diary::queries::GetDiaryQuery;
pub async fn execute(
diary: &Arc<dyn DiaryQuery>,
deps: &GetDiaryDeps,
query: GetDiaryQuery,
) -> Result<Paginated<DiaryEntry>, DomainError> {
let page = PageParams::new(query.limit, query.offset)?;
@@ -29,7 +27,7 @@ pub async fn execute(
include_remote: user_id.is_some(),
};
diary.query_diary(&filter).await
deps.diary.query_diary(&filter).await
}
#[cfg(test)]

View File

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

View File

@@ -0,0 +1,61 @@
use domain::{
errors::DomainError, models::DiaryEntry, models::ReviewSortBy, value_objects::UserId,
};
use uuid::Uuid;
use crate::diary::deps::{GetDiaryDeps, GetUserFeedDeps};
use crate::diary::get_diary;
use crate::diary::queries::GetDiaryQuery;
/// The RSS feed's author line — derived the same way the deleted handler code
/// derived its page title: from the local part of the user's email, not their
/// username.
pub struct FeedAuthor {
pub display_name: String,
}
pub struct UserFeed {
pub author: FeedAuthor,
pub entries: Vec<DiaryEntry>,
}
pub async fn execute(
deps: &GetUserFeedDeps,
user_id: Uuid,
limit: u32,
) -> Result<UserFeed, DomainError> {
let user = deps
.user
.find_by_id(&UserId::from_uuid(user_id))
.await?
.ok_or_else(|| DomainError::NotFound(format!("User {user_id}")))?;
let query = GetDiaryQuery {
limit: Some(limit),
offset: Some(0),
sort_by: Some(ReviewSortBy::Descending),
movie_id: None,
user_id: Some(user_id),
};
let get_diary_deps = GetDiaryDeps {
diary: deps.diary.clone(),
};
let page = get_diary::execute(&get_diary_deps, query).await?;
let display_name = user
.email()
.value()
.split('@')
.next()
.unwrap_or("User")
.to_string();
Ok(UserFeed {
author: FeedAuthor { display_name },
entries: page.items,
})
}
#[cfg(test)]
#[path = "tests/get_user_feed.rs"]
mod tests;

View File

@@ -7,6 +7,7 @@ pub mod get_activity_feed;
pub mod get_diary;
pub mod get_movie_social_page;
pub mod get_review_history;
pub mod get_user_feed;
pub mod log_review;
pub mod movie_resolver;
pub mod queries;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use domain::errors::DomainError;
use domain::testing::InMemorySocialRepository;
use domain::value_objects::{SocialActor, SocialIdentity, UserId};
use domain::value_objects::{FollowRelation, SocialActor, SocialIdentity, UserId};
use crate::{
config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed,
@@ -66,7 +66,7 @@ async fn returns_feed_with_following_filter() {
struct FakeSocialWithFollowing(Vec<SocialActor>);
#[async_trait]
impl domain::ports::SocialQuery for FakeSocialWithFollowing {
impl domain::ports::FollowGraphQuery for FakeSocialWithFollowing {
async fn get_following(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(self.0.clone())
}
@@ -76,17 +76,24 @@ impl domain::ports::SocialQuery for FakeSocialWithFollowing {
async fn get_pending_followers(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_following(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn count_following(&self, _: &UserId) -> Result<usize, DomainError> {
Ok(0)
}
async fn count_followers(&self, _: &UserId) -> Result<usize, DomainError> {
Ok(0)
}
async fn get_blocked(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
async fn count_pending_followers(&self, _: &UserId) -> Result<usize, DomainError> {
Ok(0)
}
async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result<bool, DomainError> {
Ok(false)
async fn get_relation(
&self,
_: &UserId,
_: &SocialIdentity,
) -> Result<FollowRelation, DomainError> {
Ok(FollowRelation::default())
}
}

View File

@@ -1,14 +1,15 @@
use domain::testing::FakeDiaryQuery;
use std::sync::Arc;
use crate::{diary::get_diary, diary::queries::GetDiaryQuery};
use crate::{diary::deps::GetDiaryDeps, diary::get_diary, diary::queries::GetDiaryQuery};
#[tokio::test]
async fn returns_empty_page() {
let diary = FakeDiaryQuery::new() as Arc<dyn domain::ports::DiaryQuery>;
let deps = GetDiaryDeps { diary };
let result = get_diary::execute(
&diary,
&deps,
GetDiaryQuery {
limit: None,
offset: None,

View File

@@ -7,7 +7,10 @@ use domain::{
value_objects::{MovieTitle, ReleaseYear},
};
use crate::{diary::get_review_history, diary::queries::GetReviewHistoryQuery};
use crate::{
diary::deps::GetReviewHistoryDeps, diary::get_review_history,
diary::queries::GetReviewHistoryQuery,
};
#[tokio::test]
async fn returns_empty_history() {
@@ -23,8 +26,9 @@ async fn returns_empty_history() {
let diary = domain::testing::FakeDiaryQuery::new();
diary.seed_history(movie, vec![]);
let diary: Arc<dyn DiaryQuery> = diary;
let deps = GetReviewHistoryDeps { diary };
let (history, trend) = get_review_history::execute(&diary, GetReviewHistoryQuery { movie_id })
let (history, trend) = get_review_history::execute(&deps, GetReviewHistoryQuery { movie_id })
.await
.unwrap();

View File

@@ -0,0 +1,93 @@
use std::sync::Arc;
use uuid::Uuid;
use domain::errors::DomainError;
use domain::models::{DiaryEntry, Movie, Review, UserRole, collections::Paginated};
use domain::testing::FakeDiaryQuery;
use domain::value_objects::{Email, MovieTitle, Rating, ReleaseYear, UserId};
use crate::auth::commands::RegisterCommand;
use crate::auth::deps::RegisterDeps;
use crate::auth::register;
use crate::diary::deps::GetUserFeedDeps;
use crate::diary::get_user_feed;
use crate::test_helpers::TestContextBuilder;
async fn setup_user(b: &TestContextBuilder, email: &str, username: &str) {
let deps = RegisterDeps {
user: b.user_repo.clone(),
password_hasher: b.password_hasher.clone(),
config: b.config.clone(),
};
register::execute(
&deps,
RegisterCommand {
email: email.into(),
username: username.into(),
password: "password123".into(),
role: UserRole::Standard,
},
)
.await
.unwrap();
}
#[tokio::test]
async fn user_feed_carries_author_and_entries() {
let b = TestContextBuilder::new();
setup_user(&b, "feed@test.com", "feeduser").await;
let email = Email::new("feed@test.com".into()).unwrap();
let user = b.user_repo.find_by_email(&email).await.unwrap().unwrap();
let uid = user.id().value();
let diary = FakeDiaryQuery::new();
let movie = Movie::new(
None,
MovieTitle::new("Feed Movie".into()).unwrap(),
ReleaseYear::new(2020).unwrap(),
None,
None,
);
let review = Review::new(
movie.id().clone(),
UserId::from_uuid(uid),
Rating::new(5).unwrap(),
None,
chrono::Utc::now().naive_utc(),
None,
)
.unwrap();
diary.set_diary_page(Paginated {
items: vec![DiaryEntry::new(movie, review)],
total_count: 1,
limit: 50,
offset: 0,
});
let deps = GetUserFeedDeps {
user: b.user_repo.clone(),
diary: Arc::clone(&diary) as _,
};
let feed = get_user_feed::execute(&deps, uid, 50).await.unwrap();
assert_eq!(feed.author.display_name, "feed");
assert_eq!(feed.entries.len(), 1);
}
#[tokio::test]
async fn user_feed_is_not_found_for_unknown_user() {
let b = TestContextBuilder::new();
let deps = GetUserFeedDeps {
user: b.user_repo.clone(),
diary: b.diary_repo.clone(),
};
let err = match get_user_feed::execute(&deps, Uuid::new_v4(), 50).await {
Err(e) => e,
Ok(_) => panic!("expected Err(NotFound) for an unknown user id, got Ok"),
};
assert!(matches!(err, DomainError::NotFound(_)));
}