1use 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#[derive(Debug)]
55pub struct AppState {
56 pub app_name: Mutex<String>,
57}
58
59pub 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
76pub 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
132pub 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
145pub struct PluginSetupContext<'a> {
147 pub database_url: &'a str,
148 pub project_root: &'a Path,
149}
150
151#[async_trait::async_trait]
157pub trait ArcPlugin: Send + Sync + 'static {
158 fn name(&self) -> &'static str;
160
161 fn register(&self, builder: ArcAppBuilder) -> ArcAppBuilder;
163
164 async fn setup(&self, _context: &PluginSetupContext<'_>) -> std::io::Result<()> {
166 Ok(())
167 }
168}
169
170impl ArcAppBuilder {
171 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 pub fn register_ui(mut self, contribution: UiContribution) -> Self {
184 self.ui_contributions.push(contribution);
185 self
186 }
187
188 pub fn build_ui_registry(&self) -> Result<Option<UiRegistry>, ui::UiError> {
190 UiRegistry::build(self.ui_host.as_ref(), &self.ui_contributions)
191 }
192 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 pub fn register_projectors(mut self, projectors: Vec<ProjectorReg>) -> Self {
209 self.current_aggregate_mut().projectors.extend(projectors);
210 self
211 }
212
213 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 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 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 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 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 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 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
317pub 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
341pub 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
351pub 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}