use std::net::SocketAddr;
use std::sync::Arc;
use crate::config::AppConfig;
use crate::http::MiddlewareHook;
use crate::http::{create_router_with_state, start_server};
use crate::module::{LifecycleHook, Module, ModuleBuilder, ModuleError};
use crate::state::AppState;
pub struct ArqenApp {
state: AppState,
module_builder: ModuleBuilder,
}
impl ArqenApp {
pub fn builder() -> ArqenAppBuilder {
ArqenAppBuilder::new()
}
pub fn state(&self) -> &AppState {
&self.state
}
pub fn module_builder(&self) -> &ModuleBuilder {
&self.module_builder
}
pub fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
crate::run(self.start())
}
pub async fn start(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Err(e) = self.module_builder.init_all().await {
tracing::error!(error = %e, "module initialization failed");
return Err(e);
}
let addr: SocketAddr = format!(
"{}:{}",
self.state.config.server.host, self.state.config.server.port
)
.parse()?;
let shutdown_timeout = self.state.config.server.shutdown_timeout;
let router = create_router_with_state(self.state.clone());
tracing::info!("Starting Arqen app on {}", addr);
let server_result = tokio::select! {
result = start_server(addr, router) => result,
_ = tokio::signal::ctrl_c() => {
tracing::info!("shutdown signal received");
Ok(())
}
};
match tokio::time::timeout(shutdown_timeout, self.module_builder.shutdown_all()).await {
Ok(Err(error)) => {
tracing::error!(error = %error, "module shutdown failed");
if server_result.is_ok() {
return Err(error);
}
}
Err(_) => {
let error = std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"module shutdown exceeded {}ms",
shutdown_timeout.as_millis()
),
);
tracing::error!(error = %error, "module shutdown timed out");
if server_result.is_ok() {
return Err(Box::new(error));
}
}
Ok(Ok(())) => {}
}
server_result
}
}
pub struct ArqenAppBuilder {
name: Option<String>,
config: Option<AppConfig>,
state: Option<AppState>,
modules: Vec<Arc<dyn Module>>,
hooks: Vec<Arc<dyn LifecycleHook>>,
middleware_hooks: Vec<Arc<dyn MiddlewareHook>>,
}
impl ArqenAppBuilder {
pub fn new() -> Self {
Self {
name: None,
config: None,
state: None,
modules: Vec::new(),
hooks: Vec::new(),
middleware_hooks: Vec::new(),
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn config(mut self, config: AppConfig) -> Self {
self.config = Some(config);
self
}
pub fn state(mut self, state: AppState) -> Self {
self.state = Some(state);
self
}
pub fn module<M: Module + 'static>(mut self, m: M) -> Self {
self.modules.push(Arc::new(m));
self
}
pub fn hook<H: LifecycleHook + 'static>(mut self, hook: H) -> Self {
self.hooks.push(Arc::new(hook));
self
}
pub fn middleware_hook<H: MiddlewareHook + 'static>(mut self, hook: H) -> Self {
self.middleware_hooks.push(Arc::new(hook));
self
}
pub fn build(self) -> Result<ArqenApp, ModuleError> {
if let Some(state) = self.state {
if self.config.is_some() || !self.modules.is_empty() || !self.hooks.is_empty() {
return Err(ModuleError::StateConflict(
"state() cannot be combined with config(), module(), or hook(); use the state builder for those options".to_string(),
));
}
let hooks = self.middleware_hooks;
return Ok(ArqenApp {
state: state.with_middleware_hooks(hooks),
module_builder: ModuleBuilder::new(),
});
}
let mut builder = AppState::builder();
if let Some(config) = self.config {
builder = builder.with_config(config);
}
let mut module_builder = ModuleBuilder::new();
if !self.modules.is_empty() || !self.hooks.is_empty() {
for module in &self.modules {
module_builder = module_builder.register_arc(module.clone());
}
for hook in &self.hooks {
module_builder = module_builder.register_hook_arc(hook.clone());
}
module_builder.validate()?;
let mut tools = crate::agent::ToolRegistry::new(
self.name.as_deref().unwrap_or("arqen-app"),
env!("CARGO_PKG_VERSION"),
"An Arqen application",
"memory",
);
let mut health = crate::health::HealthRegistry::new();
module_builder.register_all(&mut tools, &mut health)?;
builder = builder
.with_tool_registry(tools)
.with_health_registry(health);
}
builder = builder.with_middleware_hooks(self.middleware_hooks);
let state = builder.build().map_err(ModuleError::from)?;
Ok(ArqenApp {
state,
module_builder,
})
}
}
impl Default for ArqenAppBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestModule;
#[async_trait::async_trait]
impl Module for TestModule {
fn name(&self) -> &str {
"test"
}
}
struct TestHook;
impl LifecycleHook for TestHook {
fn name(&self) -> &str {
"startup"
}
}
struct TestMiddlewareHook;
impl MiddlewareHook for TestMiddlewareHook {
fn name(&self) -> &str {
"request-policy"
}
}
#[test]
fn test_arqen_app_builder_no_modules() {
let app = ArqenApp::builder().build().unwrap();
assert_eq!(app.state.config.server.port, 8888);
assert_eq!(app.module_builder.module_count(), 0);
}
#[test]
fn test_arqen_app_builder_with_module() {
let app = ArqenApp::builder().module(TestModule).build().unwrap();
assert_eq!(app.state.config.server.port, 8888);
assert_eq!(app.module_builder.module_count(), 1);
}
#[test]
fn test_arqen_app_builder_with_lifecycle_hook() {
let app = ArqenApp::builder().hook(TestHook).build().unwrap();
assert_eq!(app.module_builder.module_count(), 1);
assert_eq!(app.module_builder.module_names(), vec!["startup"]);
}
#[test]
fn test_arqen_app_builder_with_middleware_hook() {
let app = ArqenApp::builder()
.middleware_hook(TestMiddlewareHook)
.build()
.unwrap();
assert_eq!(app.state.middleware_hooks.len(), 1);
assert_eq!(app.state.middleware_hooks[0].name(), "request-policy");
}
#[test]
fn test_arqen_app_builder_with_config() {
let config = AppConfig {
server: crate::config::ServerConfig {
port: 9999,
..Default::default()
},
..Default::default()
};
let app = ArqenApp::builder().config(config).build().unwrap();
assert_eq!(app.state.config.server.port, 9999);
}
#[test]
fn test_arqen_app_builder_with_explicit_state() {
let state = AppState::builder().build().unwrap();
let app = ArqenApp::builder().state(state).build().unwrap();
assert_eq!(app.state.config.server.port, 8888);
assert_eq!(app.module_builder.module_count(), 0);
}
#[test]
fn test_explicit_state_appends_request_hooks() {
let state = AppState::builder().build().unwrap();
let app = ArqenApp::builder()
.state(state)
.middleware_hook(TestMiddlewareHook)
.build()
.unwrap();
assert_eq!(app.state.middleware_hooks.len(), 1);
}
#[test]
fn test_explicit_state_rejects_lossy_builder_options() {
let state = AppState::builder().build().unwrap();
let result = ArqenApp::builder().state(state).module(TestModule).build();
assert!(matches!(result, Err(ModuleError::StateConflict(_))));
}
#[test]
fn test_arqen_app_builder_validation_error() {
struct DepModule;
#[async_trait::async_trait]
impl Module for DepModule {
fn name(&self) -> &str {
"app"
}
fn dependencies(&self) -> Vec<&str> {
vec!["nonexistent"]
}
}
let result = ArqenApp::builder().module(DepModule).build();
assert!(result.is_err());
}
#[test]
fn test_arqen_app_module_builder_accessor() {
let app = ArqenApp::builder().module(TestModule).build().unwrap();
assert_eq!(app.module_builder().module_count(), 1);
assert_eq!(app.module_builder().module_names(), vec!["test"]);
}
}