omikron/src/config.rs
2026-08-07 23:54:28 +02:00

151 lines
4.5 KiB
Rust

use std::{env, time::Duration};
use thiserror::Error;
const DEFAULT_RHO_PORT: u16 = 443;
const DEFAULT_OMEGA_HOST: &str = "tensamin.net";
const DEFAULT_OMEGA_PORT: u16 = 9187;
const DEFAULT_OMEGA_SYNC_TIMEOUT_SECONDS: u64 = 20;
const DEFAULT_OMEGA_SYNC_RETRIES: u32 = 3;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LiveKitConfig {
pub hostname: String,
pub api_key: String,
pub api_secret: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
pub rho_port: u16,
pub omega_host: String,
pub omega_port: u16,
pub omikron_id: u64,
/// Maximum duration of one route or subscription synchronization request.
pub omega_sync_timeout: Duration,
/// Number of synchronization requests before the transport is closed and
/// the normal reconnect loop starts.
pub omega_sync_retries: u32,
pub livekit: Option<LiveKitConfig>,
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum ConfigError {
#[error("{name} must be a valid {kind}")]
InvalidValue {
name: &'static str,
kind: &'static str,
},
#[error("LIVEKIT_HOSTNAME, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET must be set together")]
IncompleteLiveKitCredentials,
}
impl Config {
pub fn from_environment() -> Result<Self, ConfigError> {
let rho_port = parse_or_default("RHO_PORT", DEFAULT_RHO_PORT)?;
let omega_port = parse_or_default("OMEGA_PORT", DEFAULT_OMEGA_PORT)?;
let omikron_id = parse_or_default("ID", 0_u64)?;
// Each synchronization request is bounded by this timeout. After
// omega_sync_retries attempts, Omikron closes the authenticated
// transport so the normal reconnect loop can establish a clean state.
let omega_sync_timeout = Duration::from_secs(parse_or_default(
"OMEGA_SYNC_TIMEOUT_SECONDS",
DEFAULT_OMEGA_SYNC_TIMEOUT_SECONDS,
)?);
let omega_sync_retries =
parse_or_default("OMEGA_SYNC_RETRIES", DEFAULT_OMEGA_SYNC_RETRIES)?.max(1);
let omega_host = env::var("OMEGA_HOST")
.unwrap_or_else(|_| DEFAULT_OMEGA_HOST.to_string())
.trim()
.to_string();
if omega_host.is_empty() {
return Err(ConfigError::InvalidValue {
name: "OMEGA_HOST",
kind: "non-empty host name",
});
}
Ok(Self {
rho_port,
omega_host,
omega_port,
omikron_id,
omega_sync_timeout,
omega_sync_retries,
livekit: livekit_from_environment()?,
})
}
}
fn parse_or_default<T>(name: &'static str, default: T) -> Result<T, ConfigError>
where
T: std::str::FromStr,
{
match env::var(name) {
Ok(value) => value.trim().parse().map_err(|_| ConfigError::InvalidValue {
name,
kind: "number",
}),
Err(_) => Ok(default),
}
}
fn livekit_from_environment() -> Result<Option<LiveKitConfig>, ConfigError> {
livekit_from_values(
env::var("LIVEKIT_HOSTNAME").ok(),
env::var("LIVEKIT_API_KEY").ok(),
env::var("LIVEKIT_API_SECRET").ok(),
)
}
fn livekit_from_values(
hostname: Option<String>,
api_key: Option<String>,
api_secret: Option<String>,
) -> Result<Option<LiveKitConfig>, ConfigError> {
match (hostname, api_key, api_secret) {
(None, None, None) => Ok(None),
(Some(hostname), Some(api_key), Some(api_secret))
if !hostname.trim().is_empty()
&& !api_key.trim().is_empty()
&& !api_secret.trim().is_empty() =>
{
Ok(Some(LiveKitConfig {
hostname,
api_key,
api_secret,
}))
}
_ => Err(ConfigError::IncompleteLiveKitCredentials),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn livekit_credentials_must_be_complete() {
assert_eq!(
livekit_from_values(Some("host".to_string()), Some("key".to_string()), None),
Err(ConfigError::IncompleteLiveKitCredentials)
);
}
#[test]
fn complete_livekit_credentials_are_loaded() {
assert_eq!(
livekit_from_values(
Some("https://livekit.example".to_string()),
Some("key".to_string()),
Some("secret".to_string()),
),
Ok(Some(LiveKitConfig {
hostname: "https://livekit.example".to_string(),
api_key: "key".to_string(),
api_secret: "secret".to_string(),
}))
);
}
}