use std::net::SocketAddr;
use std::path::PathBuf;
use nodedb_types::config::TuningConfig;
use serde::{Deserialize, Serialize};
use super::checkpoint::CheckpointSettings;
use super::cluster::ClusterSettings;
use super::cold_storage::ColdStorageSettings;
use super::observability::ObservabilityConfig;
use super::retention::RetentionSettings;
use super::scheduler::SchedulerConfig;
use super::section::ServerSection;
use super::snapshot_storage::{QuarantineStorageSettings, SnapshotStorageSettings};
use super::tls::{BackupEncryptionSettings, EncryptionSettings};
use crate::config::EngineConfig;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerConfig {
#[serde(default)]
pub server: ServerSection,
#[serde(default)]
pub engines: EngineConfig,
#[serde(default)]
pub auth: crate::config::AuthConfig,
#[serde(default)]
pub encryption: Option<EncryptionSettings>,
#[serde(default)]
pub backup_encryption: Option<BackupEncryptionSettings>,
#[serde(default)]
pub checkpoint: CheckpointSettings,
#[serde(default)]
pub retention: RetentionSettings,
#[serde(default)]
pub cluster: Option<ClusterSettings>,
#[serde(default)]
pub cold_storage: Option<ColdStorageSettings>,
#[serde(default)]
pub snapshot_storage: Option<SnapshotStorageSettings>,
#[serde(default)]
pub quarantine_storage: Option<QuarantineStorageSettings>,
#[serde(default)]
pub tuning: TuningConfig,
#[serde(default)]
pub observability: ObservabilityConfig,
#[serde(default)]
pub scheduler: SchedulerConfig,
}
impl ServerConfig {
pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| crate::Error::Config {
detail: format!("failed to read config file {}: {e}", path.display()),
})?;
let parsed: Self = toml::from_str(&content).map_err(|e| crate::Error::Config {
detail: format!("invalid TOML config: {e}"),
})?;
parsed.validate()?;
Ok(parsed)
}
pub fn validate(&self) -> crate::Result<()> {
if let Some(ref jwt) = self.auth.jwt {
jwt.validate()?;
}
Ok(())
}
pub fn addr(&self, port: u16) -> SocketAddr {
SocketAddr::new(self.server.host, port)
}
pub fn native_addr(&self) -> SocketAddr {
self.addr(self.server.ports.native)
}
pub fn pgwire_addr(&self) -> SocketAddr {
self.addr(self.server.ports.pgwire)
}
pub fn http_addr(&self) -> SocketAddr {
self.addr(self.server.ports.http)
}
pub fn resp_addr(&self) -> Option<SocketAddr> {
self.server.ports.resp.map(|p| self.addr(p))
}
pub fn ilp_addr(&self) -> Option<SocketAddr> {
self.server.ports.ilp.map(|p| self.addr(p))
}
pub fn wal_dir(&self) -> PathBuf {
self.server.data_dir.join("wal")
}
pub fn segments_dir(&self) -> PathBuf {
self.server.data_dir.join("segments")
}
pub fn catalog_path(&self) -> PathBuf {
self.server.data_dir.join("system.redb")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::server::log_format::LogFormat;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn default_config_valid() {
let cfg = ServerConfig::default();
assert_eq!(cfg.server.host, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(cfg.server.ports.native, 6433);
assert_eq!(cfg.server.ports.pgwire, 6432);
assert_eq!(cfg.server.ports.http, 6480);
assert!(cfg.server.ports.resp.is_none());
assert!(cfg.server.ports.ilp.is_none());
assert!(cfg.server.data_plane_cores >= 1);
assert_eq!(cfg.server.memory_limit, 1024 * 1024 * 1024);
}
#[test]
fn config_roundtrip() {
let cfg = ServerConfig::default();
let toml_str = toml::to_string_pretty(&cfg).expect("serialize");
let _parsed: ServerConfig = toml::from_str(&toml_str).expect("deserialize");
}
#[test]
fn log_format_default_is_text() {
let cfg = ServerConfig::default();
assert_eq!(cfg.server.log_format, LogFormat::Text);
}
fn config_toml_with_log_format(value: &str) -> String {
format!("[server]\nlog_format = {value}\n")
}
#[test]
fn log_format_toml_text_parses() {
let raw = config_toml_with_log_format("\"text\"");
let cfg: ServerConfig = toml::from_str(&raw).expect("deserialize");
assert_eq!(cfg.server.log_format, LogFormat::Text);
}
#[test]
fn log_format_toml_json_parses() {
let raw = config_toml_with_log_format("\"json\"");
let cfg: ServerConfig = toml::from_str(&raw).expect("deserialize");
assert_eq!(cfg.server.log_format, LogFormat::Json);
}
#[test]
fn log_format_toml_unknown_rejected() {
let raw = config_toml_with_log_format("\"yaml\"");
let result: Result<ServerConfig, _> = toml::from_str(&raw);
assert!(result.is_err(), "unknown log_format value must be rejected");
}
#[test]
fn unknown_top_level_table_rejected() {
let raw = "[server]\n\n[server_typo]\nfoo = 1\n";
let err = toml::from_str::<ServerConfig>(raw).unwrap_err().to_string();
assert!(
err.contains("unknown field") || err.contains("server_typo"),
"unexpected error: {err}"
);
}
}