use super::{AppConfig, RuntimeKind, build_final_db_for_module, parse_module_config};
use anyhow::{Context, Result};
use std::path::PathBuf;
use url::Url;
#[must_use]
pub fn list_module_names(app: &AppConfig) -> Vec<String> {
let mut names: Vec<String> = app.modules.keys().cloned().collect();
names.sort();
names
}
pub fn render_effective_modules_config(app: &AppConfig) -> Result<serde_json::Value> {
use serde_json::json;
let home_dir = PathBuf::from(&app.server.home_dir);
if home_dir
.components()
.any(|c| c == std::path::Component::ParentDir)
{
return Err(anyhow::anyhow!("Invalid input: {}", home_dir.display()));
}
let mut modules_config = serde_json::Map::new();
for module_name in app.modules.keys() {
let mut module_entry = serde_json::Map::new();
let parsed_config = match parse_module_config(app, module_name) {
Ok(config) => config,
Err(e) => {
tracing::warn!(
module = %module_name,
error = %e,
"Failed to parse module config, skipping"
);
continue;
}
};
if let Some(runtime_config) = parsed_config.runtime {
module_entry.insert(
"runtime".to_owned(),
json!({
"type": match runtime_config.mod_type {
RuntimeKind::Local => "local",
RuntimeKind::Oop => "oop",
}
}),
);
}
if !parsed_config.config.is_null() {
module_entry.insert("config".to_owned(), parsed_config.config);
}
match build_final_db_for_module(app, module_name, &home_dir, true) {
Ok(Some((dsn, pool))) => {
let redacted_dsn = match redact_dsn_password(&dsn) {
Ok(redacted) => redacted,
Err(e) => {
tracing::warn!(
module = %module_name,
error = %e,
"Failed to redact DSN password, skipping database config for this module"
);
if !module_entry.is_empty() {
modules_config.insert(module_name.clone(), json!(module_entry));
}
continue;
}
};
let mut db_config = serde_json::Map::new();
db_config.insert("dsn".to_owned(), json!(redacted_dsn));
let mut pool_map = serde_json::Map::new();
if let Some(max_conns) = pool.max_conns {
pool_map.insert("max_conns".to_owned(), json!(max_conns));
}
if let Some(min_conns) = pool.min_conns {
pool_map.insert("min_conns".to_owned(), json!(min_conns));
}
if let Some(acquire_timeout) = pool.acquire_timeout {
pool_map.insert(
"acquire_timeout".to_owned(),
json!(format!("{}s", acquire_timeout.as_secs())),
);
}
if let Some(idle_timeout) = pool.idle_timeout {
pool_map.insert(
"idle_timeout".to_owned(),
json!(format!("{}s", idle_timeout.as_secs())),
);
}
if let Some(max_lifetime) = pool.max_lifetime {
pool_map.insert(
"max_lifetime".to_owned(),
json!(format!("{}s", max_lifetime.as_secs())),
);
}
if let Some(test_before_acquire) = pool.test_before_acquire {
pool_map.insert("test_before_acquire".to_owned(), json!(test_before_acquire));
}
if !pool_map.is_empty() {
db_config.insert("pool".to_owned(), json!(pool_map));
}
module_entry.insert("database".to_owned(), json!(db_config));
}
Ok(None) => {
}
Err(e) => {
tracing::warn!(
module = %module_name,
error = %e,
"Failed to build database config, skipping"
);
}
}
if !module_entry.is_empty() {
modules_config.insert(module_name.clone(), json!(module_entry));
}
}
Ok(json!(modules_config))
}
pub fn redact_dsn_password(dsn: &str) -> Result<String> {
if dsn.contains('@') {
let parsed = Url::parse(dsn)?;
let mut redacted_url = parsed;
if redacted_url.password().is_some() {
redacted_url.set_password(Some("***REDACTED***")).ok();
}
Ok(redacted_url.to_string())
} else {
Ok(dsn.to_owned())
}
}
pub fn dump_effective_modules_config_yaml(app: &AppConfig) -> Result<String> {
let config = render_effective_modules_config(app)?;
serde_saphyr::to_string(&config).context("Failed to serialize modules configuration to YAML")
}
pub fn dump_effective_modules_config_json(app: &AppConfig) -> Result<String> {
let config = render_effective_modules_config(app)?;
serde_json::to_string_pretty(&config)
.context("Failed to serialize modules configuration to JSON")
}