Files
movies-diary/crates/domain/src/testing/fakes/store_movie.rs
Gabriel Kaszewski f020108024
Some checks failed
CI / Check / Test (push) Has been cancelled
CI / Check / Test (pull_request) Has been cancelled
strip session-added comments
2026-10-26 10:32:38 +01:00

247 lines
8.1 KiB
Rust

use async_trait::async_trait;
use crate::{
errors::DomainError,
events::DomainEvent,
models::{
Movie, MovieFilter, MovieProfile, MovieSummary, PersistedWatchEvent, WatchEvent,
collections::{PageParams, Paginated},
},
ports::{MovieCommand, MovieProfileRepository, MovieQuery},
value_objects::{ExternalMetadataId, MovieId, MovieTitle, ReleaseYear},
};
use super::InMemoryStore;
#[async_trait]
impl MovieCommand for InMemoryStore {
async fn upsert_movie(&self, movie: &Movie, events: &[DomainEvent]) -> Result<(), DomainError> {
let mut movies = self.movies.write().unwrap();
if let Some(external) = movie.external_metadata_id() {
let taken = movies.values().any(|m| {
m.id() != movie.id()
&& m.external_metadata_id().map(|e| e.value()) == Some(external.value())
});
if taken {
return Err(DomainError::InfrastructureError(
"Database operation failed".into(),
));
}
}
movies.insert(movie.id().value(), movie.clone());
drop(movies);
self.record_events(events);
Ok(())
}
async fn delete_movie(
&self,
movie_id: &MovieId,
events: &[DomainEvent],
) -> Result<(), DomainError> {
let id = movie_id.value();
if !self.movies.read().unwrap().contains_key(&id) {
return Ok(());
}
if self
.reviews
.read()
.unwrap()
.values()
.any(|r| r.movie_id().value() == id)
{
return Err(DomainError::InfrastructureError(
"Database operation failed".into(),
));
}
self.remove_movie_with_cascades(id);
self.record_events(events);
Ok(())
}
}
impl InMemoryStore {
pub(super) fn remove_movie_with_cascades(&self, id: uuid::Uuid) {
self.movies.write().unwrap().remove(&id);
self.watchlist
.write()
.unwrap()
.retain(|(_, movie), _| *movie != id);
self.movie_profiles.write().unwrap().remove(&id);
let mut events_guard = self.watch_events.write().unwrap();
for event in events_guard.iter_mut() {
if event.movie_id().map(|m| m.value()) == Some(id) {
*event = detached_from_movie(event);
}
}
}
}
#[async_trait]
impl MovieQuery for InMemoryStore {
async fn get_movie_by_external_id(
&self,
external_metadata_id: &ExternalMetadataId,
) -> Result<Option<Movie>, DomainError> {
let store = self.movies.read().unwrap();
Ok(store
.values()
.find(|m| {
m.external_metadata_id()
.map(|e| e.value() == external_metadata_id.value())
.unwrap_or(false)
})
.cloned())
}
async fn get_movie_by_id(&self, movie_id: &MovieId) -> Result<Option<Movie>, DomainError> {
Ok(self.movies.read().unwrap().get(&movie_id.value()).cloned())
}
async fn get_movies_by_title_and_year(
&self,
title: &MovieTitle,
year: &ReleaseYear,
) -> Result<Vec<Movie>, DomainError> {
let store = self.movies.read().unwrap();
Ok(store
.values()
.filter(|m| m.title() == title && m.release_year() == year)
.cloned()
.collect())
}
async fn existing_external_ids(
&self,
ids: &[ExternalMetadataId],
) -> Result<std::collections::HashSet<String>, DomainError> {
let store = self.movies.read().unwrap();
let known: std::collections::HashSet<String> = store
.values()
.filter_map(|m| m.external_metadata_id().map(|e| e.value().to_string()))
.collect();
Ok(ids
.iter()
.map(|id| id.value().to_string())
.filter(|v| known.contains(v))
.collect())
}
async fn existing_title_year_pairs(
&self,
pairs: &[(MovieTitle, ReleaseYear)],
) -> Result<std::collections::HashSet<(String, u16)>, DomainError> {
let store = self.movies.read().unwrap();
let known: std::collections::HashSet<(String, u16)> = store
.values()
.map(|m| (m.title().value().to_string(), m.release_year().value()))
.collect();
Ok(pairs
.iter()
.map(|(t, y)| (t.value().to_string(), y.value()))
.filter(|p| known.contains(p))
.collect())
}
async fn list_movies(
&self,
page: &PageParams,
filter: &MovieFilter,
) -> Result<Paginated<MovieSummary>, DomainError> {
let movies = self.movies.read().unwrap();
let profiles = self.movie_profiles.read().unwrap();
let search = filter.search.as_deref().map(str::to_lowercase);
let mut rows: Vec<MovieSummary> = movies
.values()
.filter_map(|m| {
let profile = profiles.get(&m.id().value());
let genres: Vec<String> = profile
.map(|p| p.genres.iter().map(|g| g.name.clone()).collect())
.unwrap_or_default();
let language = profile.and_then(|p| p.original_language.clone());
let keep = search
.as_ref()
.is_none_or(|q| m.title().value().to_lowercase().contains(q))
&& filter
.language
.as_ref()
.is_none_or(|l| language.as_ref() == Some(l))
&& filter.genre.as_ref().is_none_or(|g| {
genres.iter().any(|n| n.to_lowercase() == g.to_lowercase())
});
keep.then(|| MovieSummary {
movie: m.clone(),
genres,
runtime_minutes: profile.and_then(|p| p.runtime_minutes),
original_language: language,
overview: profile.and_then(|p| p.overview.clone()),
collection_name: profile.and_then(|p| p.collection_name.clone()),
})
})
.collect();
rows.sort_by(|a, b| a.movie.title().value().cmp(b.movie.title().value()));
let total_count = rows.len() as u64;
let items = rows
.into_iter()
.skip(page.offset as usize)
.take(page.limit as usize)
.collect();
Ok(Paginated {
items,
total_count,
limit: page.limit,
offset: page.offset,
})
}
async fn list_movies_with_external_id(&self) -> Result<Vec<Movie>, DomainError> {
Ok(self
.movies
.read()
.unwrap()
.values()
.filter(|m| m.external_metadata_id().is_some())
.cloned()
.collect())
}
}
#[async_trait]
impl MovieProfileRepository for InMemoryStore {
async fn upsert(&self, profile: &MovieProfile) -> Result<(), DomainError> {
self.movie_profiles
.write()
.unwrap()
.insert(profile.movie_id.value(), profile.clone());
Ok(())
}
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError> {
Ok(self
.movie_profiles
.read()
.unwrap()
.get(&id.value())
.cloned())
}
async fn list_stale(&self) -> Result<Vec<(MovieId, String)>, DomainError> {
Ok(vec![])
}
}
fn detached_from_movie(event: &WatchEvent) -> WatchEvent {
WatchEvent::from_persistence(PersistedWatchEvent {
id: event.id().clone(),
user_id: event.user_id().clone(),
movie_id: None,
title: event.title().to_string(),
year: event.year(),
external_metadata_id: event.external_metadata_id().map(str::to_string),
source: event.source().clone(),
watched_at: *event.watched_at(),
status: event.status().clone(),
created_at: *event.created_at(),
})
}