feat(rss): implement RSS feed adapter and integrate with application state
This commit is contained in:
@@ -28,6 +28,7 @@ poster-storage = { workspace = true }
|
||||
sqlite = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
template-askama = { workspace = true }
|
||||
rss = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
|
||||
@@ -79,6 +79,11 @@ mod tests {
|
||||
fn render_diary_page(&self, _: &domain::models::collections::Paginated<domain::models::DiaryEntry>) -> Result<String, String> { panic!() }
|
||||
}
|
||||
|
||||
struct PanicRssRenderer;
|
||||
impl crate::ports::RssFeedRenderer for PanicRssRenderer {
|
||||
fn render_feed(&self, _: &[domain::models::DiaryEntry]) -> Result<String, String> { panic!() }
|
||||
}
|
||||
|
||||
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::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!() } }
|
||||
@@ -101,6 +106,7 @@ mod tests {
|
||||
config: application::config::AppConfig { allow_registration: false },
|
||||
},
|
||||
html_renderer: Arc::new(PanicRenderer),
|
||||
rss_renderer: Arc::new(PanicRssRenderer),
|
||||
};
|
||||
|
||||
let app = test_router(state);
|
||||
|
||||
@@ -75,6 +75,34 @@ pub mod html {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod rss {
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::header,
|
||||
response::IntoResponse,
|
||||
};
|
||||
|
||||
use application::{queries::GetDiaryQuery, use_cases::get_diary};
|
||||
use domain::{errors::DomainError, models::SortDirection};
|
||||
|
||||
use crate::{errors::ApiError, state::AppState};
|
||||
|
||||
pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
|
||||
let query = GetDiaryQuery {
|
||||
limit: Some(50),
|
||||
offset: Some(0),
|
||||
sort_by: Some(SortDirection::Descending),
|
||||
movie_id: None,
|
||||
};
|
||||
let page = get_diary::execute(&state.app_ctx, query).await?;
|
||||
let xml = state
|
||||
.rss_renderer
|
||||
.render_feed(&page.items)
|
||||
.map_err(|e| ApiError(DomainError::InfrastructureError(e)))?;
|
||||
Ok(([(header::CONTENT_TYPE, "application/rss+xml; charset=utf-8")], xml))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod api {
|
||||
use axum::{
|
||||
Json,
|
||||
|
||||
@@ -13,6 +13,7 @@ use metadata::MetadataClientImpl;
|
||||
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
|
||||
use poster_storage::{PosterStorageAdapter, StorageConfig};
|
||||
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
|
||||
use rss::RssAdapter;
|
||||
use template_askama::AskamaHtmlRenderer;
|
||||
|
||||
use presentation::{routes, state::AppState};
|
||||
@@ -78,6 +79,10 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
|
||||
Ok(AppState {
|
||||
app_ctx,
|
||||
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
||||
rss_renderer: Arc::new(RssAdapter::new(
|
||||
"Movie Diary".into(),
|
||||
"http://localhost:3000".into(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub use application::ports::HtmlRenderer;
|
||||
pub use application::ports::RssFeedRenderer;
|
||||
|
||||
@@ -16,6 +16,7 @@ fn html_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/diary", routing::get(handlers::html::get_diary_page))
|
||||
.route("/reviews", routing::post(handlers::html::post_review))
|
||||
.route("/feed.rss", routing::get(handlers::rss::get_feed))
|
||||
}
|
||||
|
||||
fn api_routes() -> Router<AppState> {
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::sync::Arc;
|
||||
|
||||
use application::context::AppContext;
|
||||
|
||||
use crate::ports::HtmlRenderer;
|
||||
use crate::ports::{HtmlRenderer, RssFeedRenderer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub app_ctx: AppContext,
|
||||
pub html_renderer: Arc<dyn HtmlRenderer>,
|
||||
pub rss_renderer: Arc<dyn RssFeedRenderer>,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use http_body_util::BodyExt;
|
||||
use presentation::{routes, state::AppState};
|
||||
use sqlite::SqliteMovieRepository;
|
||||
use sqlx::SqlitePool;
|
||||
use rss::RssAdapter;
|
||||
use template_askama::AskamaHtmlRenderer;
|
||||
use tower::ServiceExt;
|
||||
|
||||
@@ -105,6 +106,7 @@ async fn test_app() -> Router {
|
||||
config: AppConfig { allow_registration: false },
|
||||
},
|
||||
html_renderer: Arc::new(AskamaHtmlRenderer::new()),
|
||||
rss_renderer: Arc::new(RssAdapter::new("Movie Diary".into(), "http://localhost:3000".into())),
|
||||
};
|
||||
|
||||
routes::build_router(state)
|
||||
|
||||
Reference in New Issue
Block a user