refactor: bootstrap crate, thin server/worker binaries

This commit is contained in:
2026-10-25 21:16:38 +01:00
parent 04f9c13625
commit 9bf9342fc0
39 changed files with 939 additions and 905 deletions

View File

@@ -0,0 +1,147 @@
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Context;
use application::config::AppConfig as AppSettings;
use domain::ports::{DiaryExporter, DocumentParser, EventPublisher};
use domain::value_objects::InstanceIdentity;
use export::ExportAdapter;
use importer::ImporterDocumentParser;
use presentation::context::AppContext;
use presentation::{openapi, routes, state::AppState};
use rss::RssAdapter;
use crate::database::{self, DatabaseOutput};
use crate::{Repositories, event_bus, federation};
pub struct ServerApp {
pub router: axum::Router,
pub addr: SocketAddr,
}
/// Wires the whole HTTP application from configuration.
pub async fn build_server(config: &config::AppConfig) -> anyhow::Result<ServerApp> {
database::warn_if_database_backend_set();
let instance = InstanceIdentity::new(config.server.base_url.clone());
let db = database::build_database_adapters(&config.database.url, &instance).await?;
let router = assemble(config, db, instance)
.await
.context("Failed to wire dependencies")?;
// Resolved like `TcpListener::bind(&str)` would, so a hostname HOST keeps working.
let bind = format!("{}:{}", config.server.host, config.server.port);
let addr = tokio::net::lookup_host(&bind)
.await
.with_context(|| format!("Invalid HOST/PORT {bind}"))?
.next()
.with_context(|| format!("HOST/PORT {bind} resolved to no address"))?;
Ok(ServerApp { router, addr })
}
/// Builds the same router as [`build_server`] on a caller-provided pool.
/// Migrations run on it exactly as they do for the normal path.
pub async fn build_server_on(
config: &config::AppConfig,
pool: sqlx::SqlitePool,
) -> anyhow::Result<axum::Router> {
let instance = InstanceIdentity::new(config.server.base_url.clone());
let db = database::build_database_adapters_on(pool, &instance).await?;
assemble(config, db, instance).await
}
async fn assemble(
config: &config::AppConfig,
db: DatabaseOutput,
instance: InstanceIdentity,
) -> anyhow::Result<axum::Router> {
let app_config = AppSettings::from(config);
let (auth_service, password_hasher) = database::build_auth_adapters(&config.auth)?;
let metadata_client = database::build_metadata_client(&config.metadata)?;
let poster_fetcher = database::build_poster_fetcher(&config.metadata)?;
let object_storage = database::build_object_storage(&config.storage)?;
let event_publisher: Arc<dyn EventPublisher> =
event_bus::create_publisher(&db.db_pool, config).await?;
let social = federation::wire_server_social(&db, &instance, config, &event_publisher).await?;
let review_logger = Arc::new(application::diary::review_logger::DefaultReviewLogger::new(
Arc::clone(&db.movie_command),
Arc::clone(&db.movie_query),
Arc::clone(&db.review),
Arc::clone(&db.watchlist),
Arc::clone(&metadata_client),
Arc::clone(&event_publisher),
));
let repos = Repositories {
movie_command: db.movie_command,
movie_query: db.movie_query,
review: db.review,
diary: db.diary,
stats: db.stats,
user: db.user,
import_session: db.import_session,
import_profile: db.import_profile,
movie_profile: db.movie_profile,
watchlist: db.watchlist,
watch_event_command: db.watch_event_command,
watch_event_query: db.watch_event_query,
webhook_token: db.webhook_token,
person_command: db.person_command,
person_query: db.person_query,
search_port: db.search_port,
search_command: db.search_command,
profile_fields: db.profile_fields,
remote_watchlist: social.remote_watchlist,
social_command: social.social_command,
follow_graph: social.follow_graph,
block_query: social.block_query,
federation_admin: social.federation_admin,
wrapup_stats: db.wrapup_stats,
wrapup_repo: db.wrapup_repo,
insights: db.insights,
goal_command: db.goal_command,
goal_query: db.goal_query,
user_settings: db.user_settings,
remote_goal: db.remote_goal,
refresh_session: db.refresh_session,
federated_profile: social.federated_profile,
};
let services = application::Services {
auth: auth_service,
password_hasher,
metadata: metadata_client,
poster_fetcher,
object_storage,
event_publisher,
diary_exporter: Arc::new(ExportAdapter) as Arc<dyn DiaryExporter>,
document_parser: Arc::new(ImporterDocumentParser) as Arc<dyn DocumentParser>,
review_logger,
person_enrichment: None,
};
let deps = Arc::new(crate::build_deps(&repos, &services, &app_config, &instance));
let app_ctx = AppContext {
deps,
services,
config: app_config,
instance,
#[cfg(feature = "federation")]
ap_document: social.ap_document,
#[cfg(feature = "federation")]
ap_blocklist: social.ap_blocklist,
};
let state = AppState {
app_ctx,
rss_renderer: Arc::new(RssAdapter::new(config.server.base_url.clone())),
};
Ok(openapi::serve(routes::build_router(
state,
social.ap_router,
)))
}