refactor: move AppContext to presentation crate, structurally enforce boundary
All checks were successful
CI / Check / Test (push) Successful in 39m33s

This commit is contained in:
2026-06-11 23:18:28 +02:00
parent b5cc7f8371
commit 57520c00f3
51 changed files with 268 additions and 377 deletions

View File

@@ -0,0 +1,64 @@
use std::sync::Arc;
use domain::ports::{
AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, GoalRepository,
ImportProfileRepository, ImportSessionRepository, MetadataClient, MovieProfileRepository,
MovieRepository, ObjectStorage, PasswordHasher, PersonCommand, PersonEnrichmentClient,
PersonQuery, PosterFetcherClient, RefreshSessionRepository, RemoteGoalRepository,
RemoteWatchlistRepository, ReviewRepository, SearchCommand, SearchPort, SocialQueryPort,
StatsRepository, UserProfileFieldsRepository, UserRepository, UserSettingsRepository,
WatchEventRepository, WatchlistRepository, WebhookTokenRepository, WrapUpRepository,
WrapUpStatsQuery,
};
use application::config::AppConfig;
use application::ports::ReviewLogger;
#[derive(Clone)]
pub struct Repositories {
pub movie: Arc<dyn MovieRepository>,
pub review: Arc<dyn ReviewRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub stats: Arc<dyn StatsRepository>,
pub user: Arc<dyn UserRepository>,
pub import_session: Arc<dyn ImportSessionRepository>,
pub import_profile: Arc<dyn ImportProfileRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
pub watchlist: Arc<dyn WatchlistRepository>,
pub watch_event: Arc<dyn WatchEventRepository>,
pub webhook_token: Arc<dyn WebhookTokenRepository>,
pub person_command: Arc<dyn PersonCommand>,
pub person_query: Arc<dyn PersonQuery>,
pub search_port: Arc<dyn SearchPort>,
pub search_command: Arc<dyn SearchCommand>,
pub profile_fields: Arc<dyn UserProfileFieldsRepository>,
pub remote_watchlist: Arc<dyn RemoteWatchlistRepository>,
pub social_query: Arc<dyn SocialQueryPort>,
pub wrapup_stats: Arc<dyn WrapUpStatsQuery>,
pub wrapup_repo: Arc<dyn WrapUpRepository>,
pub goal: Arc<dyn GoalRepository>,
pub user_settings: Arc<dyn UserSettingsRepository>,
pub remote_goal: Arc<dyn RemoteGoalRepository>,
pub refresh_session: Arc<dyn RefreshSessionRepository>,
}
#[derive(Clone)]
pub struct Services {
pub auth: Arc<dyn AuthService>,
pub password_hasher: Arc<dyn PasswordHasher>,
pub metadata: Arc<dyn MetadataClient>,
pub poster_fetcher: Arc<dyn PosterFetcherClient>,
pub object_storage: Arc<dyn ObjectStorage>,
pub event_publisher: Arc<dyn EventPublisher>,
pub diary_exporter: Arc<dyn DiaryExporter>,
pub document_parser: Arc<dyn DocumentParser>,
pub review_logger: Arc<dyn ReviewLogger>,
pub person_enrichment: Option<Arc<dyn PersonEnrichmentClient>>,
}
#[derive(Clone)]
pub struct AppContext {
pub repos: Repositories,
pub services: Services,
pub config: AppConfig,
}

View File

@@ -8,10 +8,10 @@ use uuid::Uuid;
use application::diary::{
commands::DeleteReviewCommand,
delete_review, export_diary as export_diary_uc, get_activity_feed as get_feed_uc, get_diary,
log_review,
queries::{ExportQuery, GetActivityFeedQuery},
delete_review,
deps::{DeleteReviewDeps, GetActivityFeedDeps},
export_diary as export_diary_uc, get_activity_feed as get_feed_uc, get_diary, log_review,
queries::{ExportQuery, GetActivityFeedQuery},
};
use domain::models::ExportFormat;
@@ -81,7 +81,11 @@ pub async fn post_review(
Json(req): Json<LogReviewRequest>,
) -> Result<impl IntoResponse, ApiError> {
let data = LogReviewData::try_from(req).map_err(ApiError)?;
log_review::execute(&state.app_ctx.services.review_logger, data.into_command(user.0.value())).await?;
log_review::execute(
&state.app_ctx.services.review_logger,
data.into_command(user.0.value()),
)
.await?;
Ok(StatusCode::CREATED)
}
@@ -244,7 +248,12 @@ pub async fn post_review_html(
}
};
match log_review::execute(&state.app_ctx.services.review_logger, data.into_command(user_id.value())).await {
match log_review::execute(
&state.app_ctx.services.review_logger,
data.into_command(user_id.value()),
)
.await
{
Ok(_) => Redirect::to("/").into_response(),
Err(e) => {
let msg = encode_error(&e.to_string());

View File

@@ -109,18 +109,16 @@ pub async fn get_import_page(
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
let ctx = super::helpers::build_page_context(&state, Some(user_id.clone()), csrf.0).await;
let profiles = list_import_profiles::execute(
state.app_ctx.repos.import_profile.clone(),
&user_id,
)
.await
.unwrap_or_default()
.into_iter()
.map(|p| ImportProfileView {
id: p.id.value().to_string(),
name: p.name,
})
.collect::<Vec<_>>();
let profiles =
list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id)
.await
.unwrap_or_default()
.into_iter()
.map(|p| ImportProfileView {
id: p.id.value().to_string(),
name: p.name,
})
.collect::<Vec<_>>();
render_page(ImportUploadTemplate {
ctx: &ctx,
profiles: &profiles,
@@ -765,7 +763,8 @@ pub async fn api_get_profiles(
State(state): State<AppState>,
AuthenticatedUser(user_id): AuthenticatedUser,
) -> impl IntoResponse {
match list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id).await {
match list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id).await
{
Ok(profiles) => axum::Json(
profiles
.into_iter()

View File

@@ -116,7 +116,9 @@ pub async fn post_revoke_token(
user_id: user_id.value(),
token_id,
};
if let Err(e) = revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await {
if let Err(e) =
revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await
{
tracing::error!("revoke token failed: {:?}", e);
}

View File

@@ -9,16 +9,11 @@ use uuid::Uuid;
use application::{
diary::{
commands::SyncPosterCommand,
deps::{GetMovieSocialPageDeps},
deps::GetMovieSocialPageDeps,
get_movie_social_page, get_review_history,
queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery},
},
movies::{
deps::SyncPosterDeps,
get_movies,
queries::GetMoviesQuery,
sync_poster,
},
movies::{deps::SyncPosterDeps, get_movies, queries::GetMoviesQuery, sync_poster},
watchlist::{is_on as is_on_watchlist, queries::IsOnWatchlistQuery},
};
use domain::services::review_history::Trend;
@@ -88,8 +83,11 @@ pub async fn get_review_history(
State(state): State<AppState>,
Path(movie_id): Path<Uuid>,
) -> Result<Json<ReviewHistoryResponse>, ApiError> {
let (history, trend) =
get_review_history::execute(&state.app_ctx.repos.diary, GetReviewHistoryQuery { movie_id }).await?;
let (history, trend) = get_review_history::execute(
&state.app_ctx.repos.diary,
GetReviewHistoryQuery { movie_id },
)
.await?;
Ok(Json(ReviewHistoryResponse {
movie: crate::mappers::movies::movie_to_dto(history.movie()),
@@ -210,12 +208,7 @@ pub async fn get_movie_profile(
) -> impl IntoResponse {
use application::movies::get_movie_profile;
let query = get_movie_profile::GetMovieProfileQuery { movie_id };
match get_movie_profile::execute(
state.app_ctx.repos.movie_profile.clone(),
query,
)
.await
{
match get_movie_profile::execute(state.app_ctx.repos.movie_profile.clone(), query).await {
Ok(Some(result)) => {
let p = result.profile;
Json(MovieProfileResponse {

View File

@@ -15,10 +15,11 @@ use application::integrations::{
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
IngestWatchEventCommand, RevokeWebhookTokenCommand, WatchEventConfirmation,
},
confirm as confirm_watch_events, deps::IngestWatchEventDeps,
confirm as confirm_watch_events,
deps::IngestWatchEventDeps,
dismiss as dismiss_watch_events, generate_token as generate_webhook_token,
get_queue as get_watch_queue, get_tokens as get_webhook_tokens,
ingest as ingest_watch_event, queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
get_queue as get_watch_queue, get_tokens as get_webhook_tokens, ingest as ingest_watch_event,
queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
revoke_token as revoke_webhook_token,
};
use domain::models::WatchEventSource;
@@ -164,7 +165,8 @@ pub async fn post_generate_webhook_token(
label: req.label,
};
let result = generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
let result =
generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
let base_url = &state.app_ctx.config.base_url;
let webhook_url = format!("{base_url}/api/v1/webhooks/{provider}");
@@ -191,7 +193,8 @@ pub async fn get_webhook_tokens(
let query = GetWebhookTokensQuery {
user_id: user.0.value(),
};
let tokens = get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query).await?;
let tokens =
get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query).await?;
let dtos = tokens
.into_iter()
@@ -321,6 +324,7 @@ pub async fn post_dismiss_watch_events(
event_ids: req.event_ids,
};
let dismissed = dismiss_watch_events::execute(state.app_ctx.repos.watch_event.clone(), cmd).await?;
let dismissed =
dismiss_watch_events::execute(state.app_ctx.repos.watch_event.clone(), cmd).await?;
Ok(Json(DismissWatchResponse { dismissed }))
}

View File

@@ -116,9 +116,12 @@ pub async fn get_status(
_user: AuthenticatedUser,
Path(id): Path<Uuid>,
) -> Result<Json<WrapUpStatusResponse>, ApiError> {
let record = get_wrapup::execute(state.app_ctx.repos.wrapup_repo.clone(), WrapUpId::from_uuid(id))
.await?
.ok_or_else(|| DomainError::NotFound("wrap-up not found".into()))?;
let record = get_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
WrapUpId::from_uuid(id),
)
.await?
.ok_or_else(|| DomainError::NotFound("wrap-up not found".into()))?;
Ok(Json(record_to_dto(&record)))
}
@@ -138,7 +141,12 @@ pub async fn get_report(
_user: AuthenticatedUser,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
match get_wrapup::execute(state.app_ctx.repos.wrapup_repo.clone(), WrapUpId::from_uuid(id)).await {
match get_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
WrapUpId::from_uuid(id),
)
.await
{
Ok(Some(record)) if record.status == WrapUpStatus::Ready => match record.report {
Some(ref report) => {
let json = serde_json::to_string(report).unwrap_or_default();
@@ -168,7 +176,11 @@ pub async fn delete_wrapup_handler(
_admin: AdminApiUser,
Path(id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
delete_wrapup::execute(state.app_ctx.repos.wrapup_repo.clone(), WrapUpId::from_uuid(id)).await?;
delete_wrapup::execute(
state.app_ctx.repos.wrapup_repo.clone(),
WrapUpId::from_uuid(id),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}

View File

@@ -1,3 +1,4 @@
pub mod context;
pub mod csrf;
pub mod errors;
pub mod extractors;

View File

@@ -5,12 +5,10 @@ use anyhow::Context;
use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use application::{
config::AppConfig,
context::{AppContext, Repositories, Services},
};
use application::config::AppConfig;
use export::ExportAdapter;
use importer::ImporterDocumentParser;
use presentation::context::{AppContext, Repositories, Services};
use presentation::{factory, openapi, routes, state::AppState};
use rss::RssAdapter;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use application::context::AppContext;
use crate::context::AppContext;
use crate::ports::RssFeedRenderer;

View File

@@ -1,8 +1,6 @@
use super::*;
use application::{
config::AppConfig,
context::{AppContext, Repositories, Services},
};
use crate::context::{AppContext, Repositories, Services};
use application::config::AppConfig;
use axum::{
Router,
body::Body,