Skip to main content

arc_web/
lib.rs

1//! Arc framework runtime.
2//!
3//! Holds the reusable web/runtime machinery — Actix middleware, Tera helpers,
4//! the event-sourced stack wiring, the websocket transport, and the server
5//! bootstrap — behind a builder seam. Applications depend on this crate by
6//! version and plug their own aggregate, projectors, and routes in through
7//! [`ArcApp::builder`]. No concrete domain type lives here: the runtime is
8//! generic over [`arc_core::aggregate::Aggregate`].
9
10use std::marker::PhantomData;
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13use std::{env, fs};
14
15use actix_web::web::ServiceConfig;
16use arc_core::aggregate::Aggregate;
17use arc_core::command_bus::SnapshotPolicy;
18use arc_core::projection::Projector;
19use tracing::{debug, error, info};
20
21pub mod helpers {
22    pub mod access_log;
23    pub mod audit_context;
24    pub mod config;
25    pub mod csrf;
26    pub mod database;
27    pub mod es_stack;
28    pub mod general;
29    pub mod jwt;
30    pub mod rate_limit;
31    pub mod session;
32    pub mod template;
33}
34
35pub mod http {
36    pub mod middlewares {
37        pub mod auth_middleware;
38        pub mod idle_timeout_middleware;
39        pub mod jwt_middleware;
40        pub mod rate_limit_middleware;
41    }
42    pub mod errors;
43}
44
45pub mod commands;
46pub mod websocket;
47
48/// Shared application state accessible by request handlers via `web::Data`.
49#[derive(Debug)]
50pub struct AppState {
51    pub app_name: Mutex<String>,
52}
53
54/// A read-model projector plus the view (table) it maintains. Applications
55/// register these against their aggregate so the framework can drive
56/// synchronous, read-after-write projections in single-process mode.
57pub struct ProjectorReg {
58    pub projector: Box<dyn Projector>,
59    pub view: String,
60}
61
62impl ProjectorReg {
63    pub fn new(projector: impl Projector + 'static, view: impl Into<String>) -> Self {
64        Self {
65            projector: Box::new(projector),
66            view: view.into(),
67        }
68    }
69}
70
71/// Entry point to the framework. `ArcApp::builder::<MyAggregate>()` yields a
72/// builder the application configures with its aggregate, projectors, and
73/// routes, then drives with [`ArcAppBuilder::serve`].
74pub struct ArcApp;
75
76impl ArcApp {
77    pub fn builder<A: Aggregate + 'static>() -> ArcAppBuilder<A> {
78        ArcAppBuilder {
79            projectors: Vec::new(),
80            routes: None,
81            snapshot_policy: None,
82            _marker: PhantomData,
83        }
84    }
85}
86
87type RoutesFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
88
89/// Application builder, generic over the domain aggregate `A`. The concrete
90/// `A` (e.g. an application's `UserAggregate`) is supplied by the application
91/// crate; the framework never names it.
92pub struct ArcAppBuilder<A: Aggregate + 'static> {
93    projectors: Vec<ProjectorReg>,
94    routes: Option<Arc<RoutesFn>>,
95    snapshot_policy: Option<SnapshotPolicy>,
96    _marker: PhantomData<A>,
97}
98
99impl<A: Aggregate + 'static> ArcAppBuilder<A> {
100    /// Register the aggregate's read-model projectors. They are subscribed to
101    /// the in-process event bus so writes update their views synchronously.
102    pub fn register_aggregate(mut self, projectors: Vec<ProjectorReg>) -> Self {
103        self.projectors = projectors;
104        self
105    }
106
107    /// Register a single projector (chainable alternative to
108    /// [`register_aggregate`]).
109    pub fn register_projector(
110        mut self,
111        projector: impl Projector + 'static,
112        view: impl Into<String>,
113    ) -> Self {
114        self.projectors.push(ProjectorReg::new(projector, view));
115        self
116    }
117
118    /// Mount the application's own routes. Framework handlers (static assets,
119    /// websocket, health) are exported from this crate for the closure to
120    /// reference; the application owns which routes it mounts.
121    pub fn register_routes<F>(mut self, f: F) -> Self
122    where
123        F: Fn(&mut ServiceConfig) + Send + Sync + 'static,
124    {
125        self.routes = Some(Arc::new(f));
126        self
127    }
128
129    /// Snapshot policy for the aggregate's command bus (e.g. every N events).
130    pub fn snapshot_policy(mut self, policy: Option<SnapshotPolicy>) -> Self {
131        self.snapshot_policy = policy;
132        self
133    }
134
135    /// Boot the HTTP server with the configured aggregate, projectors, and
136    /// routes. Equivalent to the old `commands::serve::run`, now generic.
137    pub async fn serve(self, app_url: String, app_port: u16) -> std::io::Result<()> {
138        let routes = self
139            .routes
140            .expect("ArcAppBuilder::serve requires register_routes(..)");
141        commands::serve::run::<A>(
142            app_url,
143            app_port,
144            self.projectors,
145            self.snapshot_policy,
146            routes,
147        )
148        .await
149    }
150}
151
152/// Verifies database availability for the configured driver. For file-backed
153/// drivers (SQLite) this checks the `DATABASE_URL` file exists and exits with
154/// code 1 if missing. Connection-string drivers (Postgres) skip the check.
155pub fn check_database_health() {
156    info!("Checking database health");
157    let driver = helpers::config::DatabaseDriver::from_env();
158
159    if !driver.is_file_backed() {
160        debug!(
161            driver = driver.as_str(),
162            "Database driver uses a connection string; skipping filesystem check"
163        );
164        return;
165    }
166
167    let database: String = helpers::config::database_url();
168    if !fs::exists(PathBuf::from(&database)).unwrap() {
169        error!("Database file not found at: {}", database);
170        error!("Please run `cargo run migrate` to create the database");
171        std::process::exit(1);
172    }
173    debug!("Database file found at: {}", database);
174}
175
176/// Copies `.env.example` to `.env` when no `.env` file is present.
177pub fn check_app_health() {
178    info!("Checking app health");
179    if !fs::exists(PathBuf::from(".env")).unwrap() {
180        info!("Creating .env file from .env.example");
181        fs::copy(PathBuf::from(".env.example"), PathBuf::from(".env"))
182            .expect("Failed to copy .env.example to .env");
183    }
184}
185
186/// Fails fast if required environment variables are missing.
187pub fn validate_environment() {
188    let required_vars = ["APP_URL", "SECRET_KEY", "DATABASE_URL"];
189    let mut missing = Vec::new();
190    for var in required_vars {
191        if env::var(var).is_err() {
192            missing.push(var);
193        }
194    }
195    if !missing.is_empty() {
196        error!(
197            "Missing required environment variables: {}. Check your .env file.",
198            missing.join(", ")
199        );
200        std::process::exit(1);
201    }
202    debug!("All required environment variables present");
203}