use crate::apps::{AppConfig, AppError, Apps};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BuildError {
#[error("Application error: {0}")]
App(#[from] AppError),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Missing required configuration: {0}")]
MissingConfig(String),
#[error("Route configuration error: {0}")]
RouteError(String),
#[error("Database configuration error: {0}")]
DatabaseError(String),
}
pub type BuildResult<T> = Result<T, BuildError>;
#[derive(Clone)]
pub struct RouteConfig {
pub path: String,
pub handler_name: String,
pub name: Option<String>,
pub namespace: Option<String>,
}
impl RouteConfig {
pub fn new(path: impl Into<String>, handler_name: impl Into<String>) -> Self {
Self {
path: path.into(),
handler_name: handler_name.into(),
name: None,
namespace: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
pub fn full_name(&self) -> Option<String> {
match (&self.namespace, &self.name) {
(Some(ns), Some(name)) => Some(format!("{}:{}", ns, name)),
(None, Some(name)) => Some(name.clone()),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct ApplicationDatabaseConfig {
pub url: String,
pub pool_size: Option<u32>,
pub max_overflow: Option<u32>,
pub timeout: Option<u64>,
}
impl ApplicationDatabaseConfig {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
pool_size: None,
max_overflow: None,
timeout: None,
}
}
pub fn with_pool_size(mut self, size: u32) -> Self {
self.pool_size = Some(size);
self
}
pub fn with_max_overflow(mut self, overflow: u32) -> Self {
self.max_overflow = Some(overflow);
self
}
pub fn with_timeout(mut self, timeout: u64) -> Self {
self.timeout = Some(timeout);
self
}
}
pub struct ApplicationBuilder {
apps: Vec<AppConfig>,
middleware: Vec<String>,
url_patterns: Vec<RouteConfig>,
database_config: Option<ApplicationDatabaseConfig>,
settings: HashMap<String, String>,
}
impl ApplicationBuilder {
pub fn new() -> Self {
Self {
apps: Vec::new(),
middleware: Vec::new(),
url_patterns: Vec::new(),
database_config: None,
settings: HashMap::new(),
}
}
pub fn add_app(mut self, app: AppConfig) -> Self {
self.apps.push(app);
self
}
pub fn add_apps(mut self, apps: Vec<AppConfig>) -> Self {
self.apps.extend(apps);
self
}
pub fn add_middleware(mut self, middleware: impl Into<String>) -> Self {
self.middleware.push(middleware.into());
self
}
pub fn add_middlewares<S: Into<String>>(mut self, middleware: Vec<S>) -> Self {
self.middleware
.extend(middleware.into_iter().map(|m| m.into()));
self
}
pub fn add_url_pattern(mut self, pattern: RouteConfig) -> Self {
self.url_patterns.push(pattern);
self
}
pub fn add_url_patterns(mut self, patterns: Vec<RouteConfig>) -> Self {
self.url_patterns.extend(patterns);
self
}
pub fn database(mut self, config: ApplicationDatabaseConfig) -> Self {
self.database_config = Some(config);
self
}
pub fn add_setting(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.settings.insert(key.into(), value.into());
self
}
pub fn add_settings(mut self, settings: HashMap<String, String>) -> Self {
self.settings.extend(settings);
self
}
fn validate(&self) -> BuildResult<()> {
for app in &self.apps {
app.validate_label()?;
}
let mut labels = std::collections::HashSet::new();
for app in &self.apps {
if !labels.insert(&app.label) {
return Err(BuildError::InvalidConfig(format!(
"Duplicate app label: {}",
app.label
)));
}
}
let mut route_names = std::collections::HashSet::new();
for pattern in &self.url_patterns {
if let Some(full_name) = pattern.full_name()
&& !route_names.insert(full_name.clone())
{
return Err(BuildError::RouteError(format!(
"Duplicate route name: {}",
full_name
)));
}
}
if let Some(db_config) = &self.database_config {
reinhardt_conf::settings::database_config::validate_database_url_scheme(&db_config.url)
.map_err(BuildError::DatabaseError)?;
}
Ok(())
}
pub fn build(self) -> BuildResult<Application> {
self.validate()?;
let installed_apps: Vec<String> = self.apps.iter().map(|app| app.name.clone()).collect();
let apps_registry = Apps::new(installed_apps);
for app in &self.apps {
apps_registry.register(app.clone())?;
}
apps_registry.populate()?;
Ok(Application {
apps: self.apps,
middleware: self.middleware,
url_patterns: self.url_patterns,
database_config: self.database_config,
settings: self.settings,
apps_registry: Arc::new(apps_registry),
})
}
#[cfg(feature = "di")]
pub fn build_with_di(
self,
singleton_scope: Arc<reinhardt_di::SingletonScope>,
) -> BuildResult<Arc<Application>> {
let app = self.build()?;
let app = Arc::new(app);
singleton_scope.set(app.clone());
singleton_scope.set(app.apps_registry.clone());
Ok(app)
}
}
impl Default for ApplicationBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct Application {
apps: Vec<AppConfig>,
middleware: Vec<String>,
url_patterns: Vec<RouteConfig>,
database_config: Option<ApplicationDatabaseConfig>,
settings: HashMap<String, String>,
apps_registry: Arc<Apps>,
}
impl Application {
pub fn apps(&self) -> &[AppConfig] {
&self.apps
}
pub fn middleware(&self) -> &[String] {
&self.middleware
}
pub fn url_patterns(&self) -> &[RouteConfig] {
&self.url_patterns
}
pub fn database_config(&self) -> Option<&ApplicationDatabaseConfig> {
self.database_config.as_ref()
}
pub fn settings(&self) -> &HashMap<String, String> {
&self.settings
}
pub fn apps_registry(&self) -> &Apps {
&self.apps_registry
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn test_route_config_creation() {
let route = RouteConfig::new("/users/", "UserListHandler")
.with_name("user-list")
.with_namespace("api");
assert_eq!(route.path, "/users/");
assert_eq!(route.handler_name, "UserListHandler");
assert_eq!(route.name, Some("user-list".to_string()));
assert_eq!(route.namespace, Some("api".to_string()));
}
#[test]
fn test_route_config_full_name() {
let route = RouteConfig::new("/users/", "UserListHandler")
.with_namespace("api")
.with_name("list");
assert_eq!(route.full_name(), Some("api:list".to_string()));
let route = RouteConfig::new("/users/", "UserListHandler").with_name("list");
assert_eq!(route.full_name(), Some("list".to_string()));
let route = RouteConfig::new("/users/", "UserListHandler");
assert_eq!(route.full_name(), None);
}
#[test]
fn test_database_config_creation() {
let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb")
.with_pool_size(10)
.with_max_overflow(5)
.with_timeout(30);
assert_eq!(db_config.url, "postgresql://localhost/mydb");
assert_eq!(db_config.pool_size, Some(10));
assert_eq!(db_config.max_overflow, Some(5));
assert_eq!(db_config.timeout, Some(30));
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_basic() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new().build().unwrap();
assert_eq!(app.apps().len(), 0);
assert_eq!(app.middleware().len(), 0);
assert_eq!(app.url_patterns().len(), 0);
assert!(app.database_config().is_none());
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_apps() {
crate::registry::reset_global_registry();
let app_config = AppConfig::new("myapp", "myapp");
let app = ApplicationBuilder::new()
.add_app(app_config)
.build()
.unwrap();
assert_eq!(app.apps().len(), 1);
assert_eq!(app.apps()[0].label, "myapp");
assert!(app.apps_registry().is_installed("myapp"));
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_multiple_apps() {
crate::registry::reset_global_registry();
let apps = vec![
AppConfig::new("app1", "app1"),
AppConfig::new("app2", "app2"),
];
let app = ApplicationBuilder::new().add_apps(apps).build().unwrap();
assert_eq!(app.apps().len(), 2);
assert!(app.apps_registry().is_installed("app1"));
assert!(app.apps_registry().is_installed("app2"));
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_middleware() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new()
.add_middleware("CorsMiddleware")
.add_middleware("AuthMiddleware")
.build()
.unwrap();
assert_eq!(app.middleware().len(), 2);
assert_eq!(app.middleware()[0], "CorsMiddleware");
assert_eq!(app.middleware()[1], "AuthMiddleware");
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_middlewares() {
crate::registry::reset_global_registry();
let middleware = vec!["CorsMiddleware", "AuthMiddleware"];
let app = ApplicationBuilder::new()
.add_middlewares(middleware)
.build()
.unwrap();
assert_eq!(app.middleware().len(), 2);
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_url_patterns() {
crate::registry::reset_global_registry();
let route = RouteConfig::new("/users/", "UserListHandler");
let app = ApplicationBuilder::new()
.add_url_pattern(route)
.build()
.unwrap();
assert_eq!(app.url_patterns().len(), 1);
assert_eq!(app.url_patterns()[0].path, "/users/");
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_database() {
crate::registry::reset_global_registry();
let db_config = ApplicationDatabaseConfig::new("postgresql://localhost/mydb");
let app = ApplicationBuilder::new()
.database(db_config)
.build()
.unwrap();
assert!(app.database_config().is_some());
assert_eq!(
app.database_config().unwrap().url,
"postgresql://localhost/mydb"
);
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_with_settings() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new()
.add_setting("DEBUG", "true")
.add_setting("SECRET_KEY", "secret")
.build()
.unwrap();
assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
assert_eq!(
app.settings().get("SECRET_KEY"),
Some(&"secret".to_string())
);
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_validation_duplicate_apps() {
crate::registry::reset_global_registry();
let result = ApplicationBuilder::new()
.add_app(AppConfig::new("myapp", "myapp"))
.add_app(AppConfig::new("another", "myapp"))
.build();
assert!(result.is_err());
match result {
Err(BuildError::InvalidConfig(msg)) => {
assert_eq!(msg, "Duplicate app label: myapp");
}
_ => panic!("Expected InvalidConfig error"),
}
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_validation_duplicate_routes() {
crate::registry::reset_global_registry();
let result = ApplicationBuilder::new()
.add_url_pattern(RouteConfig::new("/users/", "Handler1").with_name("users"))
.add_url_pattern(RouteConfig::new("/posts/", "Handler2").with_name("users"))
.build();
assert!(result.is_err());
match result {
Err(BuildError::RouteError(msg)) => {
assert_eq!(msg, "Duplicate route name: users");
}
_ => panic!("Expected RouteError"),
}
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_method_chaining() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new()
.add_app(AppConfig::new("app1", "app1"))
.add_middleware("CorsMiddleware")
.add_url_pattern(RouteConfig::new("/api/", "ApiHandler"))
.database(ApplicationDatabaseConfig::new("postgresql://localhost/db"))
.add_setting("DEBUG", "true")
.build()
.unwrap();
assert_eq!(app.apps().len(), 1);
assert_eq!(app.middleware().len(), 1);
assert_eq!(app.url_patterns().len(), 1);
assert!(app.database_config().is_some());
assert_eq!(app.settings().get("DEBUG"), Some(&"true".to_string()));
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_apps_registry_ready() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new()
.add_app(AppConfig::new("myapp", "myapp"))
.build()
.unwrap();
assert!(app.apps_registry().is_ready());
assert!(app.apps_registry().is_apps_ready());
assert!(app.apps_registry().is_models_ready());
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_invalid_app_label() {
crate::registry::reset_global_registry();
let result = ApplicationBuilder::new()
.add_app(AppConfig::new("myapp", "my-app"))
.build();
assert!(result.is_err());
match result {
Err(BuildError::App(AppError::InvalidLabel(_))) => {}
_ => panic!("Expected InvalidLabel error"),
}
}
#[test]
fn test_route_config_without_name() {
let route = RouteConfig::new("/api/v1/users/", "UserHandler");
assert_eq!(route.full_name(), None);
}
#[test]
fn test_database_config_minimal() {
let db_config = ApplicationDatabaseConfig::new("sqlite::memory:");
assert_eq!(db_config.url, "sqlite::memory:");
assert_eq!(db_config.pool_size, None);
assert_eq!(db_config.max_overflow, None);
assert_eq!(db_config.timeout, None);
}
#[rstest::rstest]
#[case::postgres("postgres://localhost/db")]
#[case::postgresql("postgresql://user:pass@localhost:5432/db")]
#[case::sqlite_memory("sqlite::memory:")]
#[case::sqlite_absolute("sqlite:///var/data/db.sqlite3")]
#[case::sqlite_relative("sqlite:db.sqlite3")]
#[case::mysql("mysql://root@localhost/db")]
#[case::mariadb("mariadb://root@localhost/db")]
#[serial(apps_registry)]
fn test_application_builder_accepts_valid_database_url_scheme(#[case] url: &str) {
crate::registry::reset_global_registry();
let db_config = ApplicationDatabaseConfig::new(url);
let result = ApplicationBuilder::new().database(db_config).build();
assert!(
result.is_ok(),
"expected URL {:?} to be accepted but got {:?}",
url,
result.err()
);
}
#[rstest::rstest]
#[case::empty("")]
#[case::not_a_url("not a url")]
#[case::http("http://localhost/db")]
#[case::ftp("ftp://localhost/db")]
#[case::redis("redis://localhost")]
#[case::missing_scheme("localhost/db")]
fn test_application_builder_rejects_invalid_database_url_scheme(#[case] url: &str) {
let db_config = ApplicationDatabaseConfig::new(url);
let result = ApplicationBuilder::new().database(db_config).build();
match result {
Err(BuildError::DatabaseError(msg)) => {
assert!(
msg.contains("Invalid database URL"),
"unexpected error message for {:?}: {}",
url,
msg
);
}
Err(other) => panic!("expected BuildError::DatabaseError, got {:?}", other),
Ok(_) => panic!("expected build to fail for invalid URL: {:?}", url),
}
}
#[test]
#[serial(apps_registry)]
fn test_application_builder_empty_settings() {
crate::registry::reset_global_registry();
let app = ApplicationBuilder::new().build().unwrap();
assert!(app.settings().is_empty());
}
}