use crate::error::VecboostError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use confers::Config;
#[cfg(feature = "db")]
use super::app::DatabaseConfig;
use super::app::{
AuditConfig, AuthConfig, DeviceConfig, EmbeddingConfig, MemoryPagingConfig, MemoryPoolConfig,
ModelConfig, MonitoringConfig, RateLimitConfig, RerankConfig, SemanticCacheConfig,
ServerConfig, apply_priority_defaults, apply_security_env_overrides,
};
use crate::pipeline::PipelineConfig;
#[derive(Config, Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[config(env_prefix = "VECBOOST_")]
#[serde(default)]
pub struct VecboostConfig {
pub server: ServerConfig,
pub model: ModelConfig,
pub embedding: EmbeddingConfig,
pub rerank: RerankConfig,
pub monitoring: MonitoringConfig,
pub auth: AuthConfig,
pub rate_limit: RateLimitConfig,
pub audit: AuditConfig,
pub memory_pool: MemoryPoolConfig,
pub pipeline: PipelineConfig,
pub logging: crate::config::app::LoggingConfig,
pub semantic_cache: SemanticCacheConfig,
pub memory_paging: MemoryPagingConfig,
pub device: DeviceConfig,
#[cfg(feature = "db")]
pub database: DatabaseConfig,
}
impl VecboostConfig {
pub fn load_via_confers() -> Result<Self, VecboostError> {
Self::load_via_confers_with_path("config/config.toml")
}
pub fn load_via_confers_with_path<P: Into<PathBuf>>(path: P) -> Result<Self, VecboostError> {
let mut config = confers::ConfigBuilder::<Self>::new()
.allow_absolute_paths()
.file_optional(path)
.env_prefix("VECBOOST_")
.build()
.map_err(|e| VecboostError::ConfigError(format!("confers: {e}")))?;
apply_security_env_overrides(&mut config)?;
apply_priority_defaults(&mut config.pipeline.priority);
config.validate()?;
Ok(config)
}
pub fn generate_schema() -> Result<String, VecboostError> {
confers::schema::TypeScriptGenerator::generate::<Self>()
.map_err(|e| VecboostError::ConfigError(format!("schema generation failed: {e}")))
}
pub fn validate(&self) -> Result<(), VecboostError> {
use garde::Validate;
let mut errors = Vec::new();
let mut collect = |prefix: &str, result: Result<(), garde::Report>| {
if let Err(report) = result {
for (path, error) in report.iter() {
errors.push(format!("{prefix}.{path}: {error}"));
}
}
};
collect("server", self.server.validate());
collect("model", self.model.validate());
collect("embedding", self.embedding.validate());
collect("rerank", self.rerank.validate());
if errors.is_empty() {
Ok(())
} else {
Err(VecboostError::ConfigError(format!(
"Configuration validation failed:\n {}",
errors.join("\n ")
)))
}
}
}
impl trait_kit::kit::Configurable for VecboostConfig {
fn load() -> Result<Self, Box<dyn std::error::Error + Send + 'static>> {
Self::load_via_confers().map_err(|e| Box::new(e) as _)
}
}
#[cfg(test)]
mod tests {
use super::VecboostConfig;
use crate::config::app::test_support::ENV_LOCK;
#[test]
fn test_confers_app_config_compiles() {
let _ = VecboostConfig::default();
}
#[test]
fn test_app_config_default_values() {
let config = VecboostConfig::default();
assert!(!config.server.grpc_enabled);
assert!(config.model.batch_size > 0);
assert!(config.embedding.max_batch_size > 0);
}
#[test]
fn test_load_via_confers_with_nonexistent_path() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
assert!(result.is_ok(), "should fall back to defaults");
let config = result.unwrap();
assert!(!config.server.grpc_enabled);
}
#[test]
fn test_load_via_confers_with_valid_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("test_config.toml");
std::fs::write(
&config_path,
r#"
[server]
host = "0.0.0.0"
port = 8080
grpc_enabled = true
[model]
model_repo = "test/repo"
batch_size = 64
[embedding]
max_batch_size = 128
"#,
)
.expect("Failed to write config");
let result = VecboostConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.server.port, 8080);
assert_eq!(config.server.host, "0.0.0.0");
assert!(config.server.grpc_enabled);
assert_eq!(config.model.batch_size, 64);
assert_eq!(config.embedding.max_batch_size, 128);
}
#[test]
fn test_load_via_confers_with_empty_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("empty.toml");
std::fs::write(&config_path, "").expect("Failed to write empty config");
let result = VecboostConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert!(!config.server.grpc_enabled);
}
#[test]
fn test_load_via_confers_jwt_secret_env_override() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var(
"VECBOOST_JWT_SECRET",
"this-is-a-valid-jwt-secret-32chars!!",
);
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
let config = result.expect("load should succeed with valid JWT secret");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert_eq!(
config.auth.jwt_secret.as_deref(),
Some("this-is-a-valid-jwt-secret-32chars!!")
);
}
#[test]
fn test_load_via_confers_empty_jwt_secret_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_JWT_SECRET", "");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert!(
result.is_err(),
"empty JWT secret must be rejected by apply_security_env_overrides"
);
}
#[test]
fn test_load_via_confers_short_jwt_secret_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_JWT_SECRET", "tooshort");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
}
assert!(
result.is_err(),
"JWT secret shorter than 32 chars must be rejected"
);
}
#[test]
fn test_load_via_confers_admin_password_env_override() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "SuperSecurePass123!");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
let config = result.expect("load should succeed with valid admin password");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert_eq!(
config.auth.default_admin_password.as_deref(),
Some("SuperSecurePass123!")
);
}
#[test]
fn test_load_via_confers_empty_admin_password_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert!(
result.is_err(),
"empty admin password must be rejected by apply_security_env_overrides"
);
}
#[test]
fn test_load_via_confers_short_admin_password_rejected() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("VECBOOST_ADMIN_PASSWORD", "short");
}
let result = VecboostConfig::load_via_confers_with_path("/nonexistent/config.toml");
unsafe {
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
assert!(
result.is_err(),
"admin password shorter than 12 chars must be rejected"
);
}
#[test]
fn test_load_via_confers_with_partial_toml() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("partial.toml");
std::fs::write(
&config_path,
r#"
[server]
port = 9999
"#,
)
.expect("Failed to write partial config");
let result = VecboostConfig::load_via_confers_with_path(&config_path);
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.server.port, 9999);
}
#[test]
fn test_app_config_clone() {
let config = VecboostConfig::default();
let cloned = config.clone();
assert_eq!(config.server.port, cloned.server.port);
}
#[test]
fn test_app_config_debug_format() {
let config = VecboostConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("VecboostConfig"));
}
#[test]
fn test_app_config_validate_default_succeeds() {
let config = VecboostConfig::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_app_config_validate_bad_server_port() {
let mut config = VecboostConfig::default();
config.server.port = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_app_config_serialize_roundtrip() {
let config = VecboostConfig::default();
let json = serde_json::to_string(&config).unwrap();
let deserialized: VecboostConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.server.port, config.server.port);
}
#[test]
fn test_app_config_generate_schema() {
let result = VecboostConfig::generate_schema();
assert!(result.is_ok());
let schema = result.unwrap();
assert!(!schema.is_empty());
}
#[test]
fn test_app_config_load_via_confers_default() {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let result = VecboostConfig::load_via_confers();
assert!(result.is_ok());
}
#[tokio::test]
async fn test_app_config_configurable_load() {
use trait_kit::kit::Configurable;
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("VECBOOST_JWT_SECRET");
std::env::remove_var("VECBOOST_ADMIN_PASSWORD");
}
let result = <VecboostConfig as Configurable>::load();
assert!(result.is_ok());
}
}