use crate::route::{RouteTable, Upstream};
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigToml {
#[serde(default = "default_health_interval")]
pub health_interval_secs: u64,
#[serde(default = "default_cooldown")]
pub default_cooldown_secs: u64,
#[serde(default = "default_rps")]
pub tenant_rps: u32,
pub tls: TlsToml,
pub jwt: JwtToml,
pub routes: Vec<RouteToml>,
}
fn default_health_interval() -> u64 {
5
}
fn default_cooldown() -> u64 {
30
}
fn default_rps() -> u32 {
1_000
}
#[derive(Debug, Clone, Deserialize)]
pub struct TlsToml {
pub cert_path: String,
pub key_path: String,
pub client_ca_path: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct JwtToml {
pub jwks_path: String,
#[serde(default)]
pub issuer: Option<String>,
#[serde(default)]
pub audience: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RouteToml {
pub prefix: String,
pub upstream: String,
#[serde(default)]
pub cooldown_secs: Option<u64>,
}
pub fn build_table(cfg: &ConfigToml) -> anyhow::Result<RouteTable> {
let default_cooldown = cfg.default_cooldown_secs;
let mut rules = Vec::with_capacity(cfg.routes.len());
for r in &cfg.routes {
let uri: http::Uri = r.upstream.parse()?;
if uri.authority().is_none() {
anyhow::bail!(
"route {:?}: upstream {:?} has no authority (scheme://host)",
r.prefix,
r.upstream
);
}
let cooldown = r.cooldown_secs.unwrap_or(default_cooldown);
anyhow::ensure!(
cooldown > 0,
"route {}: cooldown_secs must be at least 1",
r.prefix
);
rules.push((r.prefix.clone(), Upstream::new(uri, cooldown)));
}
Ok(RouteTable::new(rules))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repo_config_toml_parses_and_builds() {
let raw = include_str!("../../../config.toml");
let cfg: ConfigToml = toml::from_str(raw).expect("config.toml should parse");
build_table(&cfg).expect("build_table should succeed for config.toml");
}
#[test]
fn build_table_rejects_upstream_without_authority() {
let cfg = ConfigToml {
health_interval_secs: 5,
default_cooldown_secs: 30,
tenant_rps: 1000,
tls: TlsToml {
cert_path: "certs/server.crt".into(),
key_path: "certs/server.key".into(),
client_ca_path: "certs/ca-bundle.crt".into(),
},
jwt: JwtToml {
jwks_path: "certs/jwt-pub.pem".into(),
issuer: None,
audience: None,
},
routes: vec![RouteToml {
prefix: "/svc-a".into(),
upstream: "/no-authority-here".into(),
cooldown_secs: None,
}],
};
assert!(build_table(&cfg).is_err());
}
#[test]
fn build_table_rejects_zero_cooldown() {
let raw = include_str!("../../../config.toml").replace(
"upstream = \"http://localhost:8001\"",
"upstream = \"http://localhost:8001\"\ncooldown_secs = 0",
);
let cfg: ConfigToml = toml::from_str(&raw).unwrap();
assert!(build_table(&cfg).is_err());
}
}