From edcf3c1170cc1b71b69f04d2964c280287cf1bda Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 4 May 2026 11:51:20 +0200 Subject: [PATCH] feat(poster-fetcher): add poster fetcher adapter with configuration and integration --- .env.example | 1 + Cargo.lock | 11 ++++++ Cargo.toml | 3 +- crates/adapters/poster-fetcher/Cargo.toml | 10 ++++++ crates/adapters/poster-fetcher/src/config.rs | 13 +++++++ crates/adapters/poster-fetcher/src/lib.rs | 38 ++++++++++++++++++++ crates/presentation/Cargo.toml | 1 + crates/presentation/src/main.rs | 21 ++--------- 8 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 crates/adapters/poster-fetcher/Cargo.toml create mode 100644 crates/adapters/poster-fetcher/src/config.rs create mode 100644 crates/adapters/poster-fetcher/src/lib.rs diff --git a/.env.example b/.env.example index 9ca8ec7..547faf0 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ JWT_SECRET= JWT_TTL_SECONDS= ALLOW_REGISTRATION=true OMDB_API_KEY= +POSTER_FETCH_TIMEOUT_SECONDS=30 MINIO_ENDPOINT= MINIO_ACCESS_KEY_ID= MINIO_SECRET_ACCESS_KEY= diff --git a/Cargo.lock b/Cargo.lock index 9d72ff8..5a8dbd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1620,6 +1620,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "poster-fetcher" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "domain", + "reqwest 0.13.3", +] + [[package]] name = "poster-storage" version = "0.1.0" @@ -1671,6 +1681,7 @@ dependencies = [ "dotenvy", "http-body-util", "metadata", + "poster-fetcher", "poster-storage", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 35a2225..52b4a24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ "crates/adapters/auth", - "crates/adapters/metadata", "crates/adapters/poster-storage", + "crates/adapters/metadata", "crates/adapters/poster-fetcher", "crates/adapters/poster-storage", "crates/adapters/rss", "crates/adapters/sqlite", "crates/adapters/template-askama", @@ -39,6 +39,7 @@ application = { path = "crates/application" } presentation = { path = "crates/presentation" } auth = { path = "crates/adapters/auth" } metadata = { path = "crates/adapters/metadata" } +poster-fetcher = { path = "crates/adapters/poster-fetcher" } poster-storage = { path = "crates/adapters/poster-storage" } rss = { path = "crates/adapters/rss" } sqlite = { path = "crates/adapters/sqlite" } diff --git a/crates/adapters/poster-fetcher/Cargo.toml b/crates/adapters/poster-fetcher/Cargo.toml new file mode 100644 index 0000000..4d8ab67 --- /dev/null +++ b/crates/adapters/poster-fetcher/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "poster-fetcher" +version = "0.1.0" +edition = "2021" + +[dependencies] +domain = { workspace = true } +async-trait = { workspace = true } +reqwest = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/adapters/poster-fetcher/src/config.rs b/crates/adapters/poster-fetcher/src/config.rs new file mode 100644 index 0000000..def31db --- /dev/null +++ b/crates/adapters/poster-fetcher/src/config.rs @@ -0,0 +1,13 @@ +pub struct PosterFetcherConfig { + pub timeout_seconds: u64, +} + +impl PosterFetcherConfig { + pub fn from_env() -> Self { + let timeout_seconds = std::env::var("POSTER_FETCH_TIMEOUT_SECONDS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(30); + Self { timeout_seconds } + } +} diff --git a/crates/adapters/poster-fetcher/src/lib.rs b/crates/adapters/poster-fetcher/src/lib.rs new file mode 100644 index 0000000..571619c --- /dev/null +++ b/crates/adapters/poster-fetcher/src/lib.rs @@ -0,0 +1,38 @@ +mod config; +pub use config::PosterFetcherConfig; + +use std::time::Duration; + +use async_trait::async_trait; +use domain::{errors::DomainError, ports::PosterFetcherClient, value_objects::PosterUrl}; + +pub struct ReqwestPosterFetcher { + client: reqwest::Client, +} + +impl ReqwestPosterFetcher { + pub fn new(config: PosterFetcherConfig) -> anyhow::Result { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(config.timeout_seconds)) + .build()?; + Ok(Self { client }) + } +} + +#[async_trait] +impl PosterFetcherClient for ReqwestPosterFetcher { + async fn fetch_poster_bytes(&self, poster_url: &PosterUrl) -> Result, DomainError> { + let bytes = self + .client + .get(poster_url.value()) + .send() + .await + .map_err(|e| DomainError::InfrastructureError(e.to_string()))? + .error_for_status() + .map_err(|e| DomainError::InfrastructureError(e.to_string()))? + .bytes() + .await + .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; + Ok(bytes.to_vec()) + } +} diff --git a/crates/presentation/Cargo.toml b/crates/presentation/Cargo.toml index f4f6c34..8199f8b 100644 --- a/crates/presentation/Cargo.toml +++ b/crates/presentation/Cargo.toml @@ -23,6 +23,7 @@ domain = { workspace = true } application = { workspace = true } auth = { workspace = true } metadata = { workspace = true } +poster-fetcher = { workspace = true } poster-storage = { workspace = true } sqlite = { workspace = true } sqlx = { workspace = true } diff --git a/crates/presentation/src/main.rs b/crates/presentation/src/main.rs index 94c8afd..1fee34c 100644 --- a/crates/presentation/src/main.rs +++ b/crates/presentation/src/main.rs @@ -2,12 +2,7 @@ use std::sync::Arc; use anyhow::Context; use async_trait::async_trait; -use domain::{ - errors::DomainError, - events::DomainEvent, - ports::{EventPublisher, PosterFetcherClient}, - value_objects::PosterUrl, -}; +use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher}; use sqlx::SqlitePool; use tokio::net::TcpListener; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -15,23 +10,13 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use application::{config::AppConfig, context::AppContext}; use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService}; use metadata::MetadataClientImpl; +use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher}; use poster_storage::{PosterStorageAdapter, StorageConfig}; use sqlite::{SqliteMovieRepository, SqliteUserRepository}; use template_askama::AskamaHtmlRenderer; use presentation::{routes, state::AppState}; -struct StubPosterFetcher; - -#[async_trait] -impl PosterFetcherClient for StubPosterFetcher { - async fn fetch_poster_bytes(&self, _url: &PosterUrl) -> Result, DomainError> { - Err(DomainError::InfrastructureError( - "poster fetcher not implemented".into(), - )) - } -} - struct StubEventPublisher; #[async_trait] @@ -81,7 +66,7 @@ async fn wire_dependencies() -> anyhow::Result { let app_ctx = AppContext { repository: Arc::new(movie_repo), metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)), - poster_fetcher: Arc::new(StubPosterFetcher), + poster_fetcher: Arc::new(ReqwestPosterFetcher::new(PosterFetcherConfig::from_env())?), poster_storage: Arc::new(PosterStorageAdapter::from_config(storage_config)?), event_publisher: Arc::new(StubEventPublisher), auth_service: Arc::new(JwtAuthService::new(auth_config)),