feat(metadata): Implement OMDB metadata provider and refactor metadata client

- Added `OmdbProvider` to fetch movie metadata from the OMDB API.
- Refactored `MetadataClient` to use `MetadataSearchCriteria` for fetching movie metadata.
- Updated `MetadataClientImpl` to support fetching metadata using OMDB.
- Modified `log_review` use case to utilize the new metadata fetching mechanism.
- Updated tests and presentation layer to accommodate changes in metadata handling.
- Added dependencies for `reqwest` and `async-trait` in relevant `Cargo.toml` files.
This commit is contained in:
2026-05-04 11:19:51 +02:00
parent 93c65cd155
commit da72ab1446
12 changed files with 827 additions and 54 deletions

View File

@@ -22,6 +22,7 @@ async-trait = { workspace = true }
domain = { workspace = true }
application = { workspace = true }
auth = { workspace = true }
metadata = { workspace = true }
sqlite = { workspace = true }
sqlx = { workspace = true }
template-askama = { workspace = true }

View File

@@ -80,7 +80,7 @@ mod tests {
}
struct PanicMeta; struct PanicFetcher; struct PanicStorage; struct PanicEvent; struct PanicHasher; struct PanicAuth; struct PanicUserRepo;
#[async_trait::async_trait] impl domain::ports::MetadataClient for PanicMeta { async fn fetch_movie_metadata(&self, _: &domain::value_objects::ExternalMetadataId) -> Result<domain::models::Movie, domain::errors::DomainError> { panic!() } async fn get_poster_url(&self, _: &domain::value_objects::ExternalMetadataId) -> Result<Option<domain::value_objects::PosterUrl>, domain::errors::DomainError> { panic!() } }
#[async_trait::async_trait] impl domain::ports::MetadataClient for PanicMeta { async fn fetch_movie_metadata(&self, _: &domain::ports::MetadataSearchCriteria) -> Result<domain::models::Movie, domain::errors::DomainError> { panic!() } async fn get_poster_url(&self, _: &domain::value_objects::ExternalMetadataId) -> Result<Option<domain::value_objects::PosterUrl>, domain::errors::DomainError> { panic!() } }
#[async_trait::async_trait] impl domain::ports::PosterFetcherClient for PanicFetcher { async fn fetch_poster_bytes(&self, _: &domain::value_objects::PosterUrl) -> Result<Vec<u8>, domain::errors::DomainError> { panic!() } }
#[async_trait::async_trait] impl domain::ports::PosterStorage for PanicStorage { async fn store_poster(&self, _: &domain::value_objects::MovieId, _: &[u8]) -> Result<domain::value_objects::PosterPath, domain::errors::DomainError> { panic!() } async fn get_poster(&self, _: &domain::value_objects::PosterPath) -> Result<Vec<u8>, domain::errors::DomainError> { panic!() } }
#[async_trait::async_trait] impl domain::ports::EventPublisher for PanicEvent { async fn publish(&self, _: &domain::events::DomainEvent) -> Result<(), domain::errors::DomainError> { panic!() } }

View File

@@ -5,9 +5,8 @@ use async_trait::async_trait;
use domain::{
errors::DomainError,
events::DomainEvent,
models::Movie,
ports::{EventPublisher, MetadataClient, PosterFetcherClient, PosterStorage},
value_objects::{ExternalMetadataId, MovieId, PosterPath, PosterUrl},
ports::{EventPublisher, PosterFetcherClient, PosterStorage},
value_objects::{MovieId, PosterPath, PosterUrl},
};
use sqlx::SqlitePool;
use tokio::net::TcpListener;
@@ -15,31 +14,12 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use application::{config::AppConfig, context::AppContext};
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
use metadata::MetadataClientImpl;
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
use template_askama::AskamaHtmlRenderer;
use presentation::{routes, state::AppState};
struct StubMetadataClient;
#[async_trait]
impl MetadataClient for StubMetadataClient {
async fn fetch_movie_metadata(&self, _id: &ExternalMetadataId) -> Result<Movie, DomainError> {
Err(DomainError::InfrastructureError(
"metadata client not implemented".into(),
))
}
async fn get_poster_url(
&self,
_id: &ExternalMetadataId,
) -> Result<Option<PosterUrl>, DomainError> {
Err(DomainError::InfrastructureError(
"metadata client not implemented".into(),
))
}
}
struct StubPosterFetcher;
#[async_trait]
@@ -102,6 +82,7 @@ async fn main() -> anyhow::Result<()> {
async fn wire_dependencies() -> anyhow::Result<AppState> {
let auth_config = AuthConfig::from_env()?;
let app_config = AppConfig::from_env();
let omdb_api_key = std::env::var("OMDB_API_KEY").context("OMDB_API_KEY must be set")?;
let pool = SqlitePool::connect("sqlite://reviews.db")
.await
@@ -118,7 +99,7 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
let app_ctx = AppContext {
repository: Arc::new(movie_repo),
metadata_client: Arc::new(StubMetadataClient),
metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)),
poster_fetcher: Arc::new(StubPosterFetcher),
poster_storage: Arc::new(StubPosterStorage),
event_publisher: Arc::new(StubEventPublisher),

View File

@@ -12,8 +12,8 @@ use domain::{
events::DomainEvent,
models::{Movie, User},
ports::{
AuthService, EventPublisher, GeneratedToken, MetadataClient, PasswordHasher,
PosterFetcherClient, PosterStorage, UserRepository,
AuthService, EventPublisher, GeneratedToken, MetadataClient, MetadataSearchCriteria,
PasswordHasher, PosterFetcherClient, PosterStorage, UserRepository,
},
value_objects::{
Email, ExternalMetadataId, MovieId, PasswordHash, PosterPath, PosterUrl, UserId,
@@ -37,7 +37,7 @@ impl EventPublisher for NoopEventPublisher {
struct PanicMeta;
#[async_trait]
impl MetadataClient for PanicMeta {
async fn fetch_movie_metadata(&self, _: &ExternalMetadataId) -> Result<Movie, DomainError> {
async fn fetch_movie_metadata(&self, _: &MetadataSearchCriteria) -> Result<Movie, DomainError> {
panic!("metadata not wired in tests")
}
async fn get_poster_url(&self, _: &ExternalMetadataId) -> Result<Option<PosterUrl>, DomainError> {