refactor: presentation becomes http-axum adapter, absorbs templates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "presentation"
|
||||
name = "http-axum"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
description = "Self-hosted movie diary with REST API and ActivityPub federation"
|
||||
@@ -33,7 +33,7 @@ futures = { workspace = true }
|
||||
api-types = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
application = { workspace = true }
|
||||
template-askama = { workspace = true }
|
||||
askama = { version = "0.16.0" }
|
||||
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
|
||||
utoipa-scalar = { version = "0.3.0", features = ["axum"], default-features = false }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] }
|
||||
@@ -10,6 +10,7 @@ use application::auth::{
|
||||
commands::RegisterCommand, login as login_uc, queries::LoginCommand, register as register_uc,
|
||||
};
|
||||
|
||||
use crate::html::templates::{LoginTemplate, RegisterTemplate};
|
||||
use crate::{
|
||||
csrf::{CsrfToken, secure_flag},
|
||||
errors::ApiError,
|
||||
@@ -21,7 +22,6 @@ use api_types::HtmlPageContext;
|
||||
use api_types::{
|
||||
LoginRequest, LoginResponse, LogoutRequest, RefreshRequest, RefreshResponse, RegisterRequest,
|
||||
};
|
||||
use template_askama::{LoginTemplate, RegisterTemplate};
|
||||
|
||||
// ── HTML helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -12,6 +12,7 @@ use application::diary::{
|
||||
queries::GetActivityFeedQuery,
|
||||
};
|
||||
|
||||
use crate::html::templates::{ActivityFeedTemplate, NewReviewTemplate, build_page_items};
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
@@ -24,7 +25,6 @@ use api_types::{
|
||||
ActivityFeedQueryParams, ActivityFeedResponse, DiaryQueryParams, DiaryResponse,
|
||||
EditReviewRequest, ExportQueryParams, LogReviewRequest,
|
||||
};
|
||||
use template_askama::{ActivityFeedTemplate, NewReviewTemplate, build_page_items};
|
||||
|
||||
use super::helpers::{build_export_response, build_page_context, encode_domain_error};
|
||||
|
||||
@@ -11,6 +11,10 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::html::templates::{
|
||||
ImportMappingTemplate, ImportPreviewRow, ImportPreviewTemplate, ImportProfileView,
|
||||
ImportUploadTemplate,
|
||||
};
|
||||
use crate::render::render_page;
|
||||
use application::import::{
|
||||
apply_mapping as apply_import_mapping, apply_profile_and_map,
|
||||
@@ -28,10 +32,6 @@ use domain::models::{
|
||||
import::{DomainField, Transform},
|
||||
};
|
||||
use domain::value_objects::ImportSessionId;
|
||||
use template_askama::{
|
||||
ImportMappingTemplate, ImportPreviewRow, ImportPreviewTemplate, ImportProfileView,
|
||||
ImportUploadTemplate,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
@@ -18,11 +18,11 @@ use application::integrations::{
|
||||
revoke_token as revoke_webhook_token,
|
||||
};
|
||||
|
||||
use crate::html::templates::{IntegrationsTemplate, WatchQueueTemplate};
|
||||
use crate::{
|
||||
csrf::CsrfToken, extractors::RequiredCookieUser, forms::ErrorQuery, render::render_page,
|
||||
state::AppState,
|
||||
};
|
||||
use template_askama::{IntegrationsTemplate, WatchQueueTemplate};
|
||||
|
||||
use super::helpers::{build_page_context, encode_domain_error};
|
||||
|
||||
@@ -46,7 +46,7 @@ pub async fn get_integrations_page(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let token_views: Vec<template_askama::WebhookTokenView> = tokens
|
||||
let token_views: Vec<crate::html::templates::WebhookTokenView> = tokens
|
||||
.iter()
|
||||
.map(crate::mappers::integrations::webhook_token_view)
|
||||
.collect();
|
||||
@@ -146,7 +146,7 @@ pub async fn get_watch_queue_page(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let entries: Vec<template_askama::WatchQueueDisplayEntry> = events
|
||||
let entries: Vec<crate::html::templates::WatchQueueDisplayEntry> = events
|
||||
.iter()
|
||||
.map(crate::mappers::integrations::watch_queue_entry)
|
||||
.collect();
|
||||
@@ -17,6 +17,7 @@ use application::{
|
||||
};
|
||||
use domain::services::review_history::Trend;
|
||||
|
||||
use crate::html::templates::MovieDetailTemplate;
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
@@ -29,7 +30,6 @@ use api_types::{
|
||||
MovieStatsDto, MoviesQueryParams, MoviesResponse, PaginationQueryParams, ReviewHistoryResponse,
|
||||
SocialFeedResponse, SocialReviewDto,
|
||||
};
|
||||
use template_askama::MovieDetailTemplate;
|
||||
|
||||
use super::helpers::build_page_context;
|
||||
|
||||
@@ -6,6 +6,9 @@ use axum::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::html::templates::{BlockedActorsTemplate, BlockedDomainsTemplate};
|
||||
use crate::html::templates::{FollowersTemplate, FollowingTemplate, RemoteActorData};
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
@@ -26,9 +29,6 @@ use api_types::{
|
||||
#[cfg(feature = "federation")]
|
||||
use api_types::{AddBlockedDomainRequest, BlockedActorResponse, BlockedDomainResponse};
|
||||
use domain::value_objects::{FollowTarget, InstanceIdentity, SocialActor, SocialIdentity};
|
||||
#[cfg(feature = "federation")]
|
||||
use template_askama::{BlockedActorsTemplate, BlockedDomainsTemplate};
|
||||
use template_askama::{FollowersTemplate, FollowingTemplate, RemoteActorData};
|
||||
|
||||
use super::helpers::{build_page_context, encode_domain_error};
|
||||
|
||||
@@ -917,9 +917,9 @@ pub async fn get_blocked_domains_page(
|
||||
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
|
||||
match state.app_ctx.ap_blocklist.get_blocked_domains().await {
|
||||
Ok(domains) => {
|
||||
let entries: Vec<template_askama::BlockedDomainEntry> = domains
|
||||
let entries: Vec<crate::html::templates::BlockedDomainEntry> = domains
|
||||
.into_iter()
|
||||
.map(|d| template_askama::BlockedDomainEntry {
|
||||
.map(|d| crate::html::templates::BlockedDomainEntry {
|
||||
domain: d.domain,
|
||||
reason: d.reason,
|
||||
blocked_at: d.blocked_at,
|
||||
@@ -1008,9 +1008,9 @@ pub async fn get_blocked_actors_page(
|
||||
.await
|
||||
{
|
||||
Ok(blocked) => {
|
||||
let entries: Vec<template_askama::BlockedActorEntry> = blocked
|
||||
let entries: Vec<crate::html::templates::BlockedActorEntry> = blocked
|
||||
.into_iter()
|
||||
.map(|a| template_askama::BlockedActorEntry {
|
||||
.map(|a| crate::html::templates::BlockedActorEntry {
|
||||
url: instance.actor_url_of(&a.identity),
|
||||
handle: a.handle,
|
||||
display_name: a.display_name,
|
||||
@@ -16,6 +16,11 @@ use application::users::{
|
||||
};
|
||||
use domain::{errors::DomainError, value_objects::Username};
|
||||
|
||||
use crate::html::templates::{
|
||||
EmbedProfileTemplate, MonthlyRatingRow, ProfileSettingsTemplate, ProfileTemplate,
|
||||
RemoteActorData, RemoteActorDisplay, UserSummaryView, UsersTemplate, bar_height_px,
|
||||
build_heatmap, build_page_items,
|
||||
};
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
@@ -28,11 +33,6 @@ use api_types::{
|
||||
ProfileResponse, UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserSummaryDto,
|
||||
UserTrendsDto, UsersResponse, WatchMediumStatDto,
|
||||
};
|
||||
use template_askama::{
|
||||
EmbedProfileTemplate, MonthlyRatingRow, ProfileSettingsTemplate, ProfileTemplate,
|
||||
RemoteActorData, RemoteActorDisplay, UserSummaryView, UsersTemplate, bar_height_px,
|
||||
build_heatmap, build_page_items,
|
||||
};
|
||||
|
||||
use super::goals::goal_with_progress_to_dto;
|
||||
use super::helpers::build_page_context;
|
||||
@@ -502,7 +502,7 @@ struct PaginationInfo {
|
||||
offset: u32,
|
||||
has_more: bool,
|
||||
limit: u32,
|
||||
page_items: Vec<template_askama::PageItem>,
|
||||
page_items: Vec<crate::html::templates::PageItem>,
|
||||
}
|
||||
|
||||
fn compute_pagination(
|
||||
@@ -571,7 +571,7 @@ fn build_monthly_rating_rows<'a>(
|
||||
async fn fetch_profile_goals(
|
||||
state: &AppState,
|
||||
user_id: Uuid,
|
||||
) -> Vec<template_askama::GoalViewData> {
|
||||
) -> Vec<crate::html::templates::GoalViewData> {
|
||||
let goals_list = application::goals::list::execute(
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery { user_id },
|
||||
@@ -580,7 +580,7 @@ async fn fetch_profile_goals(
|
||||
.unwrap_or_default();
|
||||
goals_list
|
||||
.iter()
|
||||
.map(|g| template_askama::GoalViewData {
|
||||
.map(|g| crate::html::templates::GoalViewData {
|
||||
year: g.goal.year(),
|
||||
target_count: g.goal.target_count(),
|
||||
current_count: g.current_count,
|
||||
@@ -18,6 +18,7 @@ use application::{
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
|
||||
use crate::html::templates::WatchlistTemplate;
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
@@ -29,7 +30,6 @@ use api_types::{
|
||||
AddToWatchlistRequest, PaginationQueryParams, WatchlistEntryDto, WatchlistResponse,
|
||||
WatchlistStatusResponse,
|
||||
};
|
||||
use template_askama::WatchlistTemplate;
|
||||
|
||||
use super::helpers::{build_page_context, encode_error};
|
||||
|
||||
@@ -219,7 +219,7 @@ fn render_wrapup(
|
||||
.take(8)
|
||||
.map(|g| g.count as f64 / genre_max as f64 * 100.0)
|
||||
.collect();
|
||||
let tmpl = template_askama::WrapUpPageTemplate {
|
||||
let tmpl = crate::html::templates::WrapUpPageTemplate {
|
||||
ctx,
|
||||
report,
|
||||
year_label: year.to_string(),
|
||||
1
crates/adapters/http-axum/src/html/mod.rs
Normal file
1
crates/adapters/http-axum/src/html/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod templates;
|
||||
@@ -136,7 +136,7 @@ pub struct MovieDetailTemplate<'a> {
|
||||
pub struct WatchlistTemplate<'a> {
|
||||
pub ctx: &'a HtmlPageContext,
|
||||
pub owner_id: uuid::Uuid,
|
||||
pub display_entries: &'a [crate::WatchlistDisplayEntry],
|
||||
pub display_entries: &'a [WatchlistDisplayEntry],
|
||||
pub current_offset: u32,
|
||||
pub has_more: bool,
|
||||
pub limit: u32,
|
||||
@@ -4,6 +4,7 @@ pub mod errors;
|
||||
pub mod extractors;
|
||||
pub mod forms;
|
||||
pub mod handlers;
|
||||
pub mod html;
|
||||
pub mod mappers;
|
||||
pub mod openapi;
|
||||
pub mod render;
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::html::templates::{ImportPreviewRow, ImportRowStatus};
|
||||
use domain::models::AnnotatedRow;
|
||||
use domain::models::import::RowResult;
|
||||
use template_askama::{ImportPreviewRow, ImportRowStatus};
|
||||
|
||||
pub fn annotated_to_preview_row(idx: usize, annotated: &AnnotatedRow) -> ImportPreviewRow {
|
||||
match &annotated.result {
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::html::templates::{WatchQueueDisplayEntry, WebhookTokenView};
|
||||
use domain::models::{WatchEvent, WebhookToken};
|
||||
use template_askama::{WatchQueueDisplayEntry, WebhookTokenView};
|
||||
|
||||
pub fn webhook_token_view(t: &WebhookToken) -> WebhookTokenView {
|
||||
WebhookTokenView {
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::html::templates::{RemoteActorData, RemoteActorDisplay, UserSummaryView};
|
||||
use application::users::get_local_profile::PendingFollowerView;
|
||||
use domain::models::RemoteActorInfo;
|
||||
use domain::models::UserSummary;
|
||||
use template_askama::{RemoteActorData, RemoteActorDisplay, UserSummaryView};
|
||||
|
||||
pub fn user_summary_view(u: &UserSummary) -> UserSummaryView {
|
||||
let name = u
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::html::templates::WatchlistDisplayEntry;
|
||||
use domain::models::{RemoteWatchlistEntry, WatchlistWithMovie, collections::Paginated};
|
||||
use template_askama::WatchlistDisplayEntry;
|
||||
|
||||
pub struct WatchlistPageResult {
|
||||
pub display_entries: Vec<WatchlistDisplayEntry>,
|
||||
@@ -3,7 +3,7 @@ use axum::{
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub fn render_page(template: impl template_askama::askama::Template) -> Response {
|
||||
pub fn render_page(template: impl askama::Template) -> Response {
|
||||
match template.render() {
|
||||
Ok(html) => Html(html).into_response(),
|
||||
Err(e) => {
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "template-askama"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.16.0" }
|
||||
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
domain = { workspace = true }
|
||||
api-types = { workspace = true }
|
||||
@@ -8,7 +8,7 @@ default = []
|
||||
nats = ["dep:nats"]
|
||||
federation = [
|
||||
"application/federation",
|
||||
"presentation/federation",
|
||||
"http-axum/federation",
|
||||
"dep:sqlite-federation",
|
||||
"dep:activitypub",
|
||||
]
|
||||
@@ -17,7 +17,7 @@ federation = [
|
||||
config = { workspace = true }
|
||||
domain = { workspace = true }
|
||||
application = { workspace = true }
|
||||
presentation = { path = "../presentation", default-features = false }
|
||||
http-axum = { path = "../adapters/http-axum", default-features = false }
|
||||
auth = { workspace = true }
|
||||
metadata = { workspace = true }
|
||||
poster-fetcher = { workspace = true }
|
||||
|
||||
@@ -6,9 +6,9 @@ use application::config::AppConfig as AppSettings;
|
||||
use domain::ports::{DiaryExporter, DocumentParser, EventPublisher};
|
||||
use domain::value_objects::InstanceIdentity;
|
||||
use export::ExportAdapter;
|
||||
use http_axum::context::AppContext;
|
||||
use http_axum::{openapi, routes, state::AppState};
|
||||
use importer::ImporterDocumentParser;
|
||||
use presentation::context::AppContext;
|
||||
use presentation::{openapi, routes, state::AppState};
|
||||
use rss::RssAdapter;
|
||||
|
||||
use crate::database::{self, DatabaseOutput};
|
||||
|
||||
@@ -10,7 +10,7 @@ pub fn init_tracing(default_directives: &str) {
|
||||
|
||||
/// `RUST_LOG` names the crates to trace in detail; anything it doesn't name
|
||||
/// still reports warnings and errors. Without a baseline, a filter like
|
||||
/// `presentation=debug` silently drops adapter errors (e.g. the sqlx detail
|
||||
/// `http_axum=debug` silently drops adapter errors (e.g. the sqlx detail
|
||||
/// behind "Database operation failed").
|
||||
fn log_filter(default: &str) -> tracing_subscriber::EnvFilter {
|
||||
let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| default.to_string());
|
||||
|
||||
@@ -25,7 +25,7 @@ tracing = { workspace = true }
|
||||
[dev-dependencies]
|
||||
domain = { workspace = true, features = ["test-helpers"] }
|
||||
application = { workspace = true }
|
||||
presentation = { path = "../presentation", default-features = false }
|
||||
http-axum = { path = "../adapters/http-axum", default-features = false }
|
||||
rss = { workspace = true }
|
||||
sqlite = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
|
||||
@@ -21,9 +21,9 @@ use domain::{
|
||||
},
|
||||
value_objects::{Email, ExternalMetadataId, PasswordHash, PosterUrl, UserId},
|
||||
};
|
||||
use http_axum::context::AppContext;
|
||||
use http_axum::{routes, state::AppState};
|
||||
use http_body_util::BodyExt;
|
||||
use presentation::context::AppContext;
|
||||
use presentation::{routes, state::AppState};
|
||||
use rss::RssAdapter;
|
||||
use sqlite::{
|
||||
SqliteDiaryRepository, SqliteInsightsQuery, SqliteMovieRepository, SqliteReviewRepository,
|
||||
|
||||
Reference in New Issue
Block a user