use std::any::TypeId;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::{env, fs};
use actix_web::web::ServiceConfig;
use arc_core::aggregate::Aggregate;
use arc_core::command_bus::SnapshotPolicy;
use arc_core::projection::Projector;
use tracing::{debug, error, info};
pub mod helpers {
pub mod access_log;
pub mod audit_context;
pub mod config;
pub mod csrf;
pub mod database;
pub mod es_stack;
pub mod general;
pub mod jwt;
pub mod rate_limit;
pub mod session;
pub mod template;
}
pub mod http {
pub mod middlewares {
pub mod auth_middleware;
pub mod idle_timeout_middleware;
pub mod jwt_middleware;
pub mod rate_limit_middleware;
}
pub mod errors;
}
pub mod commands;
pub mod websocket;
#[derive(Debug)]
pub struct AppState {
pub app_name: Mutex<String>,
}
pub struct ProjectorReg {
pub projector: Box<dyn Projector>,
pub view: String,
}
impl ProjectorReg {
pub fn new(projector: impl Projector + 'static, view: impl Into<String>) -> Self {
Self {
projector: Box::new(projector),
view: view.into(),
}
}
}
pub struct ArcApp;
impl ArcApp {
pub fn builder() -> ArcAppBuilder {
ArcAppBuilder {
aggregates: Vec::new(),
routes: None,
}
}
}
type RoutesFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
type AggregateBuildFuture = Pin<
Box<
dyn Future<Output = std::io::Result<commands::serve::BuiltAggregateRuntime>>
+ Send
+ 'static,
>,
>;
type AggregateBuildFn =
dyn FnOnce(Vec<ProjectorReg>, Option<SnapshotPolicy>) -> AggregateBuildFuture + Send + 'static;
struct AggregateRegistration {
type_id: TypeId,
aggregate_type: &'static str,
projectors: Vec<ProjectorReg>,
snapshot_policy: Option<SnapshotPolicy>,
build: Box<AggregateBuildFn>,
}
impl AggregateRegistration {
fn new<A: Aggregate + 'static>() -> Self {
Self {
type_id: TypeId::of::<A>(),
aggregate_type: A::aggregate_type(),
projectors: Vec::new(),
snapshot_policy: None,
build: Box::new(|projectors, snapshot_policy| {
Box::pin(commands::serve::build_aggregate_runtime::<A>(
projectors,
snapshot_policy,
))
}),
}
}
}
pub struct ArcAppBuilder {
aggregates: Vec<AggregateRegistration>,
routes: Option<Arc<RoutesFn>>,
}
impl ArcAppBuilder {
pub fn register_aggregate<A: Aggregate + 'static>(mut self) -> Self {
assert!(
!self
.aggregates
.iter()
.any(|registration| registration.type_id == TypeId::of::<A>()),
"aggregate type {} is already registered",
A::aggregate_type()
);
self.aggregates.push(AggregateRegistration::new::<A>());
self
}
pub fn register_projectors(mut self, projectors: Vec<ProjectorReg>) -> Self {
self.current_aggregate_mut().projectors.extend(projectors);
self
}
pub fn register_projector(
mut self,
projector: impl Projector + 'static,
view: impl Into<String>,
) -> Self {
self.current_aggregate_mut()
.projectors
.push(ProjectorReg::new(projector, view));
self
}
pub fn register_routes<F>(mut self, f: F) -> Self
where
F: Fn(&mut ServiceConfig) + Send + Sync + 'static,
{
self.routes = Some(Arc::new(f));
self
}
pub fn snapshot_policy(mut self, policy: Option<SnapshotPolicy>) -> Self {
self.current_aggregate_mut().snapshot_policy = policy;
self
}
fn current_aggregate_mut(&mut self) -> &mut AggregateRegistration {
self.aggregates.last_mut().unwrap_or_else(|| {
panic!(
"register_aggregate::<A>() must be called before registering projectors or a snapshot policy"
)
})
}
pub async fn serve(self, app_url: String, app_port: u16) -> std::io::Result<()> {
if self.aggregates.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"ArcAppBuilder::serve requires at least one register_aggregate::<A>() call",
));
}
let routes = self
.routes
.expect("ArcAppBuilder::serve requires register_routes(..)");
commands::serve::run(app_url, app_port, self.aggregates, routes).await
}
}
pub fn check_database_health() {
info!("Checking database health");
let driver = helpers::config::DatabaseDriver::from_env();
if !driver.is_file_backed() {
debug!(
driver = driver.as_str(),
"Database driver uses a connection string; skipping filesystem check"
);
return;
}
let database: String = helpers::config::database_url();
if !fs::exists(PathBuf::from(&database)).unwrap() {
error!("Database file not found at: {}", database);
error!("Please run `cargo run migrate` to create the database");
std::process::exit(1);
}
debug!("Database file found at: {}", database);
}
pub fn check_app_health() {
info!("Checking app health");
if !fs::exists(PathBuf::from(".env")).unwrap() {
info!("Creating .env file from .env.example");
fs::copy(PathBuf::from(".env.example"), PathBuf::from(".env"))
.expect("Failed to copy .env.example to .env");
}
}
pub fn validate_environment() {
let required_vars = ["APP_URL", "SECRET_KEY", "DATABASE_URL"];
let mut missing = Vec::new();
for var in required_vars {
if env::var(var).is_err() {
missing.push(var);
}
}
if !missing.is_empty() {
error!(
"Missing required environment variables: {}. Check your .env file.",
missing.join(", ")
);
std::process::exit(1);
}
debug!("All required environment variables present");
}