feat(poster-fetcher): add poster fetcher adapter with configuration and integration
This commit is contained in:
@@ -4,6 +4,7 @@ JWT_SECRET=
|
|||||||
JWT_TTL_SECONDS=
|
JWT_TTL_SECONDS=
|
||||||
ALLOW_REGISTRATION=true
|
ALLOW_REGISTRATION=true
|
||||||
OMDB_API_KEY=
|
OMDB_API_KEY=
|
||||||
|
POSTER_FETCH_TIMEOUT_SECONDS=30
|
||||||
MINIO_ENDPOINT=
|
MINIO_ENDPOINT=
|
||||||
MINIO_ACCESS_KEY_ID=
|
MINIO_ACCESS_KEY_ID=
|
||||||
MINIO_SECRET_ACCESS_KEY=
|
MINIO_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -1620,6 +1620,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poster-fetcher"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"domain",
|
||||||
|
"reqwest 0.13.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poster-storage"
|
name = "poster-storage"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1671,6 +1681,7 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"poster-fetcher",
|
||||||
"poster-storage",
|
"poster-storage",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/adapters/auth",
|
"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/rss",
|
||||||
"crates/adapters/sqlite",
|
"crates/adapters/sqlite",
|
||||||
"crates/adapters/template-askama",
|
"crates/adapters/template-askama",
|
||||||
@@ -39,6 +39,7 @@ application = { path = "crates/application" }
|
|||||||
presentation = { path = "crates/presentation" }
|
presentation = { path = "crates/presentation" }
|
||||||
auth = { path = "crates/adapters/auth" }
|
auth = { path = "crates/adapters/auth" }
|
||||||
metadata = { path = "crates/adapters/metadata" }
|
metadata = { path = "crates/adapters/metadata" }
|
||||||
|
poster-fetcher = { path = "crates/adapters/poster-fetcher" }
|
||||||
poster-storage = { path = "crates/adapters/poster-storage" }
|
poster-storage = { path = "crates/adapters/poster-storage" }
|
||||||
rss = { path = "crates/adapters/rss" }
|
rss = { path = "crates/adapters/rss" }
|
||||||
sqlite = { path = "crates/adapters/sqlite" }
|
sqlite = { path = "crates/adapters/sqlite" }
|
||||||
|
|||||||
10
crates/adapters/poster-fetcher/Cargo.toml
Normal file
10
crates/adapters/poster-fetcher/Cargo.toml
Normal file
@@ -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 }
|
||||||
13
crates/adapters/poster-fetcher/src/config.rs
Normal file
13
crates/adapters/poster-fetcher/src/config.rs
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
38
crates/adapters/poster-fetcher/src/lib.rs
Normal file
38
crates/adapters/poster-fetcher/src/lib.rs
Normal file
@@ -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<Self> {
|
||||||
|
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<Vec<u8>, 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ domain = { workspace = true }
|
|||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
auth = { workspace = true }
|
auth = { workspace = true }
|
||||||
metadata = { workspace = true }
|
metadata = { workspace = true }
|
||||||
|
poster-fetcher = { workspace = true }
|
||||||
poster-storage = { workspace = true }
|
poster-storage = { workspace = true }
|
||||||
sqlite = { workspace = true }
|
sqlite = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::{
|
use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher};
|
||||||
errors::DomainError,
|
|
||||||
events::DomainEvent,
|
|
||||||
ports::{EventPublisher, PosterFetcherClient},
|
|
||||||
value_objects::PosterUrl,
|
|
||||||
};
|
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
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 application::{config::AppConfig, context::AppContext};
|
||||||
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
|
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
|
||||||
use metadata::MetadataClientImpl;
|
use metadata::MetadataClientImpl;
|
||||||
|
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
|
||||||
use poster_storage::{PosterStorageAdapter, StorageConfig};
|
use poster_storage::{PosterStorageAdapter, StorageConfig};
|
||||||
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
||||||
use template_askama::AskamaHtmlRenderer;
|
use template_askama::AskamaHtmlRenderer;
|
||||||
|
|
||||||
use presentation::{routes, state::AppState};
|
use presentation::{routes, state::AppState};
|
||||||
|
|
||||||
struct StubPosterFetcher;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PosterFetcherClient for StubPosterFetcher {
|
|
||||||
async fn fetch_poster_bytes(&self, _url: &PosterUrl) -> Result<Vec<u8>, DomainError> {
|
|
||||||
Err(DomainError::InfrastructureError(
|
|
||||||
"poster fetcher not implemented".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StubEventPublisher;
|
struct StubEventPublisher;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -81,7 +66,7 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
|
|||||||
let app_ctx = AppContext {
|
let app_ctx = AppContext {
|
||||||
repository: Arc::new(movie_repo),
|
repository: Arc::new(movie_repo),
|
||||||
metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)),
|
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)?),
|
poster_storage: Arc::new(PosterStorageAdapter::from_config(storage_config)?),
|
||||||
event_publisher: Arc::new(StubEventPublisher),
|
event_publisher: Arc::new(StubEventPublisher),
|
||||||
auth_service: Arc::new(JwtAuthService::new(auth_config)),
|
auth_service: Arc::new(JwtAuthService::new(auth_config)),
|
||||||
|
|||||||
Reference in New Issue
Block a user