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::any::TypeId;
11use std::future::Future;
12use std::path::{Path, PathBuf};
13use std::pin::Pin;
14use std::sync::{Arc, Mutex};
15use std::{env, fs};
16
17use actix_web::web::{self, ServiceConfig};
18use arc_core::aggregate::Aggregate;
19use arc_core::command_bus::SnapshotPolicy;
20use arc_core::projection::Projector;
21use tracing::{debug, error, info};
22
23pub mod helpers {
24    pub mod access_log;
25    pub mod audit_context;
26    pub mod config;
27    pub mod csrf;
28    pub mod database;
29    pub mod es_stack;
30    pub mod general;
31    pub mod jwt;
32    pub mod rate_limit;
33    pub mod session;
34    pub mod template;
35}
36
37pub mod http {
38    pub mod middlewares {
39        pub mod auth_middleware;
40        pub mod idle_timeout_middleware;
41        pub mod jwt_middleware;
42        pub mod rate_limit_middleware;
43    }
44    pub mod errors;
45}
46
47pub mod commands;
48pub mod ui;
49pub mod websocket;
50
51pub use ui::{UiContribution, UiHost, UiRegistry};
52
53/// Shared application state accessible by request handlers via `web::Data`.
54#[derive(Debug)]
55pub struct AppState {
56    pub app_name: Mutex<String>,
57}
58
59/// A read-model projector plus the view (table) it maintains. Applications
60/// register these against their aggregate so the framework can drive
61/// synchronous, read-after-write projections in single-process mode.
62pub struct ProjectorReg {
63    pub projector: Box<dyn Projector>,
64    pub view: String,
65}
66
67impl ProjectorReg {
68    pub fn new(projector: impl Projector + 'static, view: impl Into<String>) -> Self {
69        Self {
70            projector: Box::new(projector),
71            view: view.into(),
72        }
73    }
74}
75
76/// Entry point to the framework. Applications register one or more aggregate
77/// types, their projectors, and routes, then drive the result with
78/// [`ArcAppBuilder::serve`].
79pub struct ArcApp;
80
81impl ArcApp {
82    pub fn builder() -> ArcAppBuilder {
83        ArcAppBuilder {
84            aggregates: Vec::new(),
85            routes: Vec::new(),
86            app_data: Vec::new(),
87            plugin_names: Vec::new(),
88            plugins: Vec::new(),
89            ui_host: None,
90            ui_contributions: Vec::new(),
91        }
92    }
93}
94
95type RoutesFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
96type AppDataFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
97type AggregateBuildFuture = Pin<
98    Box<
99        dyn Future<Output = std::io::Result<commands::serve::BuiltAggregateRuntime>>
100            + Send
101            + 'static,
102    >,
103>;
104type AggregateBuildFn =
105    dyn FnOnce(Vec<ProjectorReg>, Option<SnapshotPolicy>) -> AggregateBuildFuture + Send + 'static;
106
107struct AggregateRegistration {
108    type_id: TypeId,
109    aggregate_type: &'static str,
110    projectors: Vec<ProjectorReg>,
111    snapshot_policy: Option<SnapshotPolicy>,
112    build: Box<AggregateBuildFn>,
113}
114
115impl AggregateRegistration {
116    fn new<A: Aggregate + 'static>() -> Self {
117        Self {
118            type_id: TypeId::of::<A>(),
119            aggregate_type: A::aggregate_type(),
120            projectors: Vec::new(),
121            snapshot_policy: None,
122            build: Box::new(|projectors, snapshot_policy| {
123                Box::pin(commands::serve::build_aggregate_runtime::<A>(
124                    projectors,
125                    snapshot_policy,
126                ))
127            }),
128        }
129    }
130}
131
132/// Application builder holding type-erased registrations for every aggregate
133/// served by the process. Each registration produces its own typed
134/// `CommandBus<A>` while sharing the configured storage backend.
135pub struct ArcAppBuilder {
136    aggregates: Vec<AggregateRegistration>,
137    routes: Vec<Arc<RoutesFn>>,
138    app_data: Vec<Arc<AppDataFn>>,
139    plugin_names: Vec<&'static str>,
140    plugins: Vec<Arc<dyn ArcPlugin>>,
141    ui_host: Option<UiHost>,
142    ui_contributions: Vec<UiContribution>,
143}
144
145/// Setup-time inputs shared by installed capability packages.
146pub struct PluginSetupContext<'a> {
147    pub database_url: &'a str,
148    pub project_root: &'a Path,
149}
150
151/// Compile-time extension package registered through [`ArcAppBuilder`].
152///
153/// Plugins remain ordinary Cargo dependencies: registration is explicit,
154/// typed, and visible in application code. Implementations may add routes,
155/// aggregates, projectors, and other builder-supported capabilities.
156#[async_trait::async_trait]
157pub trait ArcPlugin: Send + Sync + 'static {
158    /// Stable identifier used for collision detection and diagnostics.
159    fn name(&self) -> &'static str;
160
161    /// Contribute this package's capabilities to the application builder.
162    fn register(&self, builder: ArcAppBuilder) -> ArcAppBuilder;
163
164    /// Run package-owned migrations and idempotent setup work.
165    async fn setup(&self, _context: &PluginSetupContext<'_>) -> std::io::Result<()> {
166        Ok(())
167    }
168}
169
170impl ArcAppBuilder {
171    /// Register the application's sole browser UI host.
172    pub fn register_ui_host(mut self, host: UiHost) -> Self {
173        if self.ui_host.is_some() {
174            self.ui_contributions
175                .push(UiContribution::duplicate_host(host.owner));
176        } else {
177            self.ui_host = Some(host);
178        }
179        self
180    }
181
182    /// Register namespaced templates and shell metadata from a capability.
183    pub fn register_ui(mut self, contribution: UiContribution) -> Self {
184        self.ui_contributions.push(contribution);
185        self
186    }
187
188    /// Validate and build the immutable UI registry without starting a server.
189    pub fn build_ui_registry(&self) -> Result<Option<UiRegistry>, ui::UiError> {
190        UiRegistry::build(self.ui_host.as_ref(), &self.ui_contributions)
191    }
192    /// Register a writable aggregate type. Arc creates and injects a distinct
193    /// `CommandBus<A>` for every call.
194    pub fn register_aggregate<A: Aggregate + 'static>(mut self) -> Self {
195        assert!(
196            !self
197                .aggregates
198                .iter()
199                .any(|registration| registration.type_id == TypeId::of::<A>()),
200            "aggregate type {} is already registered",
201            A::aggregate_type()
202        );
203        self.aggregates.push(AggregateRegistration::new::<A>());
204        self
205    }
206
207    /// Register all projectors for the most recently registered aggregate.
208    pub fn register_projectors(mut self, projectors: Vec<ProjectorReg>) -> Self {
209        self.current_aggregate_mut().projectors.extend(projectors);
210        self
211    }
212
213    /// Register a single projector for the most recently registered aggregate.
214    pub fn register_projector(
215        mut self,
216        projector: impl Projector + 'static,
217        view: impl Into<String>,
218    ) -> Self {
219        self.current_aggregate_mut()
220            .projectors
221            .push(ProjectorReg::new(projector, view));
222        self
223    }
224
225    /// Mount the application's own routes. Framework handlers (static assets,
226    /// websocket, health) are exported from this crate for the closure to
227    /// reference; the application owns which routes it mounts.
228    pub fn register_routes<F>(mut self, f: F) -> Self
229    where
230        F: Fn(&mut ServiceConfig) + Send + Sync + 'static,
231    {
232        self.routes.push(Arc::new(f));
233        self
234    }
235
236    /// Inject shared typed state contributed by an application or plugin.
237    pub fn register_data<T>(mut self, data: Arc<T>) -> Self
238    where
239        T: ?Sized + Send + Sync + 'static,
240    {
241        let data = web::Data::from(data);
242        self.app_data.push(Arc::new(move |cfg| {
243            cfg.app_data(data.clone());
244        }));
245        self
246    }
247
248    /// Register a compile-time capability package.
249    pub fn register_plugin<P: ArcPlugin>(mut self, plugin: P) -> Self {
250        let name = plugin.name();
251        assert!(
252            !self.plugin_names.contains(&name),
253            "plugin {name} is already registered"
254        );
255        self.plugin_names.push(name);
256        let plugin = Arc::new(plugin);
257        let mut builder = plugin.register(self);
258        builder.plugins.push(plugin);
259        builder
260    }
261
262    /// Run setup hooks for every registered capability package in order.
263    pub async fn setup_plugins(&self, context: &PluginSetupContext<'_>) -> std::io::Result<()> {
264        for plugin in &self.plugins {
265            plugin.setup(context).await.map_err(|error| {
266                std::io::Error::new(
267                    error.kind(),
268                    format!("plugin {} setup failed: {error}", plugin.name()),
269                )
270            })?;
271        }
272        Ok(())
273    }
274
275    /// Snapshot policy for the most recently registered aggregate.
276    pub fn snapshot_policy(mut self, policy: Option<SnapshotPolicy>) -> Self {
277        self.current_aggregate_mut().snapshot_policy = policy;
278        self
279    }
280
281    fn current_aggregate_mut(&mut self) -> &mut AggregateRegistration {
282        self.aggregates.last_mut().unwrap_or_else(|| {
283            panic!(
284                "register_aggregate::<A>() must be called before registering projectors or a snapshot policy"
285            )
286        })
287    }
288
289    /// Boot the HTTP server with every registered aggregate and the
290    /// application's routes.
291    pub async fn serve(self, app_url: String, app_port: u16) -> std::io::Result<()> {
292        if self.aggregates.is_empty() {
293            return Err(std::io::Error::new(
294                std::io::ErrorKind::InvalidInput,
295                "ArcAppBuilder::serve requires at least one register_aggregate::<A>() call",
296            ));
297        }
298        if self.routes.is_empty() {
299            return Err(std::io::Error::new(
300                std::io::ErrorKind::InvalidInput,
301                "ArcAppBuilder::serve requires at least one route registration",
302            ));
303        }
304        let ui_registry = self.build_ui_registry().map_err(std::io::Error::other)?;
305        commands::serve::run(
306            app_url,
307            app_port,
308            self.aggregates,
309            self.routes,
310            self.app_data,
311            ui_registry.map(Arc::new),
312        )
313        .await
314    }
315}
316
317/// Verifies database availability for the configured driver. For file-backed
318/// drivers (SQLite) this checks the `DATABASE_URL` file exists and exits with
319/// code 1 if missing. Connection-string drivers (Postgres) skip the check.
320pub fn check_database_health() {
321    info!("Checking database health");
322    let driver = helpers::config::DatabaseDriver::from_env();
323
324    if !driver.is_file_backed() {
325        debug!(
326            driver = driver.as_str(),
327            "Database driver uses a connection string; skipping filesystem check"
328        );
329        return;
330    }
331
332    let database: String = helpers::config::database_url();
333    if !fs::exists(PathBuf::from(&database)).unwrap() {
334        error!("Database file not found at: {}", database);
335        error!("Please run `cargo run migrate` to create the database");
336        std::process::exit(1);
337    }
338    debug!("Database file found at: {}", database);
339}
340
341/// Copies `.env.example` to `.env` when no `.env` file is present.
342pub fn check_app_health() {
343    info!("Checking app health");
344    if !fs::exists(PathBuf::from(".env")).unwrap() {
345        info!("Creating .env file from .env.example");
346        fs::copy(PathBuf::from(".env.example"), PathBuf::from(".env"))
347            .expect("Failed to copy .env.example to .env");
348    }
349}
350
351/// Fails fast if required environment variables are missing.
352pub fn validate_environment() {
353    let required_vars = ["APP_URL", "SECRET_KEY", "DATABASE_URL"];
354    let mut missing = Vec::new();
355    for var in required_vars {
356        if env::var(var).is_err() {
357            missing.push(var);
358        }
359    }
360    if !missing.is_empty() {
361        error!(
362            "Missing required environment variables: {}. Check your .env file.",
363            missing.join(", ")
364        );
365        std::process::exit(1);
366    }
367    debug!("All required environment variables present");
368}