Files
movies-diary/crates/application/src/users/get_profile.rs
Gabriel Kaszewski 7cfa234902 refactor: SocialActor rich queries, migrate remaining handlers, slim SocialQueryPort
SocialQuery returns SocialActor (identity+handle+display_name+avatar_url)
instead of bare SocialIdentity. Migrated get_following_page,
get_followers_page, get_blocked_actors_page to use cases. Moved
get_activity_feed + get_profile from SocialQueryPort to SocialQuery.
Legacy SocialQueryPort remains only for get_users listing.
2026-07-10 15:52:37 +02:00

193 lines
5.3 KiB
Rust

use crate::users::{
deps::GetProfileDeps,
queries::{GetUserProfileQuery, ProfileView},
};
use domain::{
errors::DomainError,
models::FeedSortBy,
models::{
DiaryEntry, DiaryFilter, ReviewSortBy, UserStats, UserTrends,
collections::{PageParams, Paginated},
},
value_objects::UserId,
};
pub struct PendingFollowerView {
pub url: String,
pub handle: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
}
pub struct UserProfileData {
pub stats: UserStats,
pub entries: Option<Paginated<DiaryEntry>>,
pub history: Option<Vec<DiaryEntry>>,
pub trends: Option<UserTrends>,
pub following_count: usize,
pub followers_count: usize,
pub pending_followers: Vec<PendingFollowerView>,
}
pub async fn execute(
deps: &GetProfileDeps,
query: GetUserProfileQuery,
) -> Result<UserProfileData, DomainError> {
let user_id = UserId::from_uuid(query.user_id);
let stats = deps.stats.get_user_stats(&user_id).await?;
let (following_count, followers_count, pending_followers) =
load_social_counts(deps, &user_id, query.is_own_profile).await;
let base = |entries, history, trends| UserProfileData {
stats,
entries,
history,
trends,
following_count,
followers_count,
pending_followers,
};
match query.view {
ProfileView::History => {
let all_entries = deps.diary.get_user_history(&user_id).await?;
Ok(base(None, Some(all_entries), None))
}
ProfileView::Trends => {
let trends = deps.stats.get_user_trends(&user_id).await?;
Ok(base(None, None, Some(trends)))
}
ProfileView::Ratings | ProfileView::Recent => {
let sort_direction = feed_sort_to_direction(query.sort_by);
let filter = paged_user_filter(
user_id,
sort_direction,
query.limit,
query.offset,
query.search.clone(),
query.include_remote,
)?;
let entries = deps.diary.query_diary(&filter).await?;
Ok(base(Some(entries), None, None))
}
}
}
async fn load_social_counts(
deps: &GetProfileDeps,
user_id: &UserId,
is_own_profile: bool,
) -> (usize, usize, Vec<PendingFollowerView>) {
let following = deps
.social_query
.count_following(user_id)
.await
.unwrap_or(0);
let followers = deps
.social_query
.count_followers(user_id)
.await
.unwrap_or(0);
if !is_own_profile {
return (following, followers, vec![]);
}
let pending = deps
.social_query
.get_pending_followers(user_id)
.await
.unwrap_or_default()
.into_iter()
.map(|p| {
let url = match &p.identity {
domain::value_objects::SocialIdentity::Remote { actor_url } => actor_url.clone(),
domain::value_objects::SocialIdentity::Local(uid) => {
format!("local:{}", uid.value())
}
};
PendingFollowerView {
url,
handle: p.handle,
display_name: p.display_name,
avatar_url: p.avatar_url,
}
})
.collect();
(following, followers, pending)
}
fn feed_sort_to_direction(sort_by: FeedSortBy) -> ReviewSortBy {
match sort_by {
FeedSortBy::Date => ReviewSortBy::Descending,
FeedSortBy::DateAsc => ReviewSortBy::Ascending,
FeedSortBy::Rating => ReviewSortBy::ByRatingDesc,
FeedSortBy::RatingAsc => ReviewSortBy::ByRatingAsc,
}
}
fn paged_user_filter(
user_id: UserId,
sort_by: ReviewSortBy,
limit: Option<u32>,
offset: Option<u32>,
search: Option<String>,
include_remote: bool,
) -> Result<DiaryFilter, DomainError> {
let page = PageParams::new(limit, offset)?;
Ok(DiaryFilter {
sort_by,
page,
movie_id: None,
user_id: Some(user_id),
search,
include_remote,
})
}
#[cfg(test)]
#[path = "tests/get_profile.rs"]
mod tests;
#[cfg(test)]
mod helper_tests {
use super::*;
#[test]
fn feed_sort_to_direction_all_variants() {
use domain::models::FeedSortBy;
assert!(matches!(
feed_sort_to_direction(FeedSortBy::Date),
ReviewSortBy::Descending
));
assert!(matches!(
feed_sort_to_direction(FeedSortBy::DateAsc),
ReviewSortBy::Ascending
));
assert!(matches!(
feed_sort_to_direction(FeedSortBy::Rating),
ReviewSortBy::ByRatingDesc
));
assert!(matches!(
feed_sort_to_direction(FeedSortBy::RatingAsc),
ReviewSortBy::ByRatingAsc
));
}
#[test]
fn paged_user_filter_builds_correctly() {
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
let filter = paged_user_filter(
uid.clone(),
ReviewSortBy::Descending,
Some(20),
Some(5),
Some("blade".into()),
false,
)
.unwrap();
assert_eq!(filter.user_id.unwrap().value(), uid.value());
assert_eq!(filter.search.as_deref(), Some("blade"));
}
}