structural refactor and codebase improvements
This commit is contained in:
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
61
crates/application/src/diary/get_user_feed.rs
Normal file
61
crates/application/src/diary/get_user_feed.rs
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
93
crates/application/src/diary/tests/get_user_feed.rs
Normal file
93
crates/application/src/diary/tests/get_user_feed.rs
Normal 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(_)));
|
||||
}
|
||||
Reference in New Issue
Block a user