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 websocket;
49
50#[derive(Debug)]
52pub struct AppState {
53 pub app_name: Mutex<String>,
54}
55
56pub struct ProjectorReg {
60 pub projector: Box<dyn Projector>,
61 pub view: String,
62}
63
64impl ProjectorReg {
65 pub fn new(projector: impl Projector + 'static, view: impl Into<String>) -> Self {
66 Self {
67 projector: Box::new(projector),
68 view: view.into(),
69 }
70 }
71}
72
73pub struct ArcApp;
77
78impl ArcApp {
79 pub fn builder() -> ArcAppBuilder {
80 ArcAppBuilder {
81 aggregates: Vec::new(),
82 routes: Vec::new(),
83 app_data: Vec::new(),
84 plugin_names: Vec::new(),
85 plugins: Vec::new(),
86 }
87 }
88}
89
90type RoutesFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
91type AppDataFn = dyn Fn(&mut ServiceConfig) + Send + Sync + 'static;
92type AggregateBuildFuture = Pin<
93 Box<
94 dyn Future<Output = std::io::Result<commands::serve::BuiltAggregateRuntime>>
95 + Send
96 + 'static,
97 >,
98>;
99type AggregateBuildFn =
100 dyn FnOnce(Vec<ProjectorReg>, Option<SnapshotPolicy>) -> AggregateBuildFuture + Send + 'static;
101
102struct AggregateRegistration {
103 type_id: TypeId,
104 aggregate_type: &'static str,
105 projectors: Vec<ProjectorReg>,
106 snapshot_policy: Option<SnapshotPolicy>,
107 build: Box<AggregateBuildFn>,
108}
109
110impl AggregateRegistration {
111 fn new<A: Aggregate + 'static>() -> Self {
112 Self {
113 type_id: TypeId::of::<A>(),
114 aggregate_type: A::aggregate_type(),
115 projectors: Vec::new(),
116 snapshot_policy: None,
117 build: Box::new(|projectors, snapshot_policy| {
118 Box::pin(commands::serve::build_aggregate_runtime::<A>(
119 projectors,
120 snapshot_policy,
121 ))
122 }),
123 }
124 }
125}
126
127pub struct ArcAppBuilder {
131 aggregates: Vec<AggregateRegistration>,
132 routes: Vec<Arc<RoutesFn>>,
133 app_data: Vec<Arc<AppDataFn>>,
134 plugin_names: Vec<&'static str>,
135 plugins: Vec<Arc<dyn ArcPlugin>>,
136}
137
138pub struct PluginSetupContext<'a> {
140 pub database_url: &'a str,
141 pub project_root: &'a Path,
142}
143
144#[async_trait::async_trait]
150pub trait ArcPlugin: Send + Sync + 'static {
151 fn name(&self) -> &'static str;
153
154 fn register(&self, builder: ArcAppBuilder) -> ArcAppBuilder;
156
157 async fn setup(&self, _context: &PluginSetupContext<'_>) -> std::io::Result<()> {
159 Ok(())
160 }
161}
162
163impl ArcAppBuilder {
164 pub fn register_aggregate<A: Aggregate + 'static>(mut self) -> Self {
167 assert!(
168 !self
169 .aggregates
170 .iter()
171 .any(|registration| registration.type_id == TypeId::of::<A>()),
172 "aggregate type {} is already registered",
173 A::aggregate_type()
174 );
175 self.aggregates.push(AggregateRegistration::new::<A>());
176 self
177 }
178
179 pub fn register_projectors(mut self, projectors: Vec<ProjectorReg>) -> Self {
181 self.current_aggregate_mut().projectors.extend(projectors);
182 self
183 }
184
185 pub fn register_projector(
187 mut self,
188 projector: impl Projector + 'static,
189 view: impl Into<String>,
190 ) -> Self {
191 self.current_aggregate_mut()
192 .projectors
193 .push(ProjectorReg::new(projector, view));
194 self
195 }
196
197 pub fn register_routes<F>(mut self, f: F) -> Self
201 where
202 F: Fn(&mut ServiceConfig) + Send + Sync + 'static,
203 {
204 self.routes.push(Arc::new(f));
205 self
206 }
207
208 pub fn register_data<T>(mut self, data: Arc<T>) -> Self
210 where
211 T: ?Sized + Send + Sync + 'static,
212 {
213 let data = web::Data::from(data);
214 self.app_data.push(Arc::new(move |cfg| {
215 cfg.app_data(data.clone());
216 }));
217 self
218 }
219
220 pub fn register_plugin<P: ArcPlugin>(mut self, plugin: P) -> Self {
222 let name = plugin.name();
223 assert!(
224 !self.plugin_names.contains(&name),
225 "plugin {name} is already registered"
226 );
227 self.plugin_names.push(name);
228 let plugin = Arc::new(plugin);
229 let mut builder = plugin.register(self);
230 builder.plugins.push(plugin);
231 builder
232 }
233
234 pub async fn setup_plugins(&self, context: &PluginSetupContext<'_>) -> std::io::Result<()> {
236 for plugin in &self.plugins {
237 plugin.setup(context).await.map_err(|error| {
238 std::io::Error::new(
239 error.kind(),
240 format!("plugin {} setup failed: {error}", plugin.name()),
241 )
242 })?;
243 }
244 Ok(())
245 }
246
247 pub fn snapshot_policy(mut self, policy: Option<SnapshotPolicy>) -> Self {
249 self.current_aggregate_mut().snapshot_policy = policy;
250 self
251 }
252
253 fn current_aggregate_mut(&mut self) -> &mut AggregateRegistration {
254 self.aggregates.last_mut().unwrap_or_else(|| {
255 panic!(
256 "register_aggregate::<A>() must be called before registering projectors or a snapshot policy"
257 )
258 })
259 }
260
261 pub async fn serve(self, app_url: String, app_port: u16) -> std::io::Result<()> {
264 if self.aggregates.is_empty() {
265 return Err(std::io::Error::new(
266 std::io::ErrorKind::InvalidInput,
267 "ArcAppBuilder::serve requires at least one register_aggregate::<A>() call",
268 ));
269 }
270 if self.routes.is_empty() {
271 return Err(std::io::Error::new(
272 std::io::ErrorKind::InvalidInput,
273 "ArcAppBuilder::serve requires at least one route registration",
274 ));
275 }
276 commands::serve::run(
277 app_url,
278 app_port,
279 self.aggregates,
280 self.routes,
281 self.app_data,
282 )
283 .await
284 }
285}
286
287pub fn check_database_health() {
291 info!("Checking database health");
292 let driver = helpers::config::DatabaseDriver::from_env();
293
294 if !driver.is_file_backed() {
295 debug!(
296 driver = driver.as_str(),
297 "Database driver uses a connection string; skipping filesystem check"
298 );
299 return;
300 }
301
302 let database: String = helpers::config::database_url();
303 if !fs::exists(PathBuf::from(&database)).unwrap() {
304 error!("Database file not found at: {}", database);
305 error!("Please run `cargo run migrate` to create the database");
306 std::process::exit(1);
307 }
308 debug!("Database file found at: {}", database);
309}
310
311pub fn check_app_health() {
313 info!("Checking app health");
314 if !fs::exists(PathBuf::from(".env")).unwrap() {
315 info!("Creating .env file from .env.example");
316 fs::copy(PathBuf::from(".env.example"), PathBuf::from(".env"))
317 .expect("Failed to copy .env.example to .env");
318 }
319}
320
321pub fn validate_environment() {
323 let required_vars = ["APP_URL", "SECRET_KEY", "DATABASE_URL"];
324 let mut missing = Vec::new();
325 for var in required_vars {
326 if env::var(var).is_err() {
327 missing.push(var);
328 }
329 }
330 if !missing.is_empty() {
331 error!(
332 "Missing required environment variables: {}. Check your .env file.",
333 missing.join(", ")
334 );
335 std::process::exit(1);
336 }
337 debug!("All required environment variables present");
338}