use clap::Parser;
use config::{Config, ConfigError};
use serde::Deserialize;
use std::env;
use std::path::PathBuf;
use std::collections::HashMap;
const DEFAULT_CONFIG: &str = include_str!("../config/default.toml");
const DEFAULT_CONFIG_PATH: &str = "/etc/hyprstream/config.toml";
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct CliArgs {
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(long, env = "HYPRSTREAM_SERVER_HOST")]
host: Option<String>,
#[arg(long, env = "HYPRSTREAM_SERVER_PORT")]
port: Option<u16>,
#[arg(long, env = "HYPRSTREAM_LOG_LEVEL")]
log_level: Option<String>,
#[arg(long, env = "HYPRSTREAM_ENGINE")]
engine: Option<String>,
#[arg(long, env = "HYPRSTREAM_ENGINE_CONNECTION")]
engine_connection: Option<String>,
#[arg(long, env = "HYPRSTREAM_ENGINE_OPTIONS")]
engine_options: Option<Vec<String>>,
#[arg(long, env = "HYPRSTREAM_ENABLE_CACHE")]
enable_cache: Option<bool>,
#[arg(long, env = "HYPRSTREAM_CACHE_ENGINE")]
cache_engine: Option<String>,
#[arg(long, env = "HYPRSTREAM_CACHE_CONNECTION")]
cache_connection: Option<String>,
#[arg(long, env = "HYPRSTREAM_CACHE_OPTIONS")]
cache_options: Option<Vec<String>>,
#[arg(long, env = "HYPRSTREAM_CACHE_MAX_DURATION")]
cache_max_duration: Option<u64>,
#[arg(long, env = "HYPRSTREAM_ENGINE_USERNAME")]
engine_username: Option<String>,
#[arg(long, env = "HYPRSTREAM_ENGINE_PASSWORD")]
engine_password: Option<String>,
#[arg(long, env = "HYPRSTREAM_CACHE_USERNAME")]
cache_username: Option<String>,
#[arg(long, env = "HYPRSTREAM_CACHE_PASSWORD")]
cache_password: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Settings {
pub server: ServerConfig,
pub engine: EngineConfig,
pub cache: CacheConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
#[serde(default = "default_log_level")]
pub log_level: String,
}
fn default_log_level() -> String {
"info".to_string()
}
#[derive(Debug, Deserialize)]
pub struct EngineConfig {
pub engine: String,
pub connection: String,
#[serde(default)]
pub options: std::collections::HashMap<String, String>,
#[serde(skip)]
pub credentials: Option<Credentials>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct CacheConfig {
pub enabled: bool,
pub engine: String,
pub connection: String,
pub options: HashMap<String, String>,
#[serde(default)]
pub credentials: Option<Credentials>,
#[serde(default = "default_ttl")]
pub ttl: Option<u64>,
}
fn default_ttl() -> Option<u64> {
Some(3600) }
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: false,
engine: "duckdb".to_string(),
connection: ":memory:".to_string(),
options: HashMap::new(),
credentials: None,
ttl: default_ttl(),
}
}
}
fn default_cache_engine() -> String {
"duckdb".to_string()
}
fn default_cache_connection() -> String {
":memory:".to_string()
}
fn default_cache_duration() -> u64 {
3600
}
impl Settings {
pub fn new(cli: CliArgs) -> Result<Self, ConfigError> {
let mut builder = Config::builder();
builder = builder.add_source(config::File::from_str(
DEFAULT_CONFIG,
config::FileFormat::Toml,
));
if let Ok(metadata) = std::fs::metadata(DEFAULT_CONFIG_PATH) {
if metadata.is_file() {
builder = builder.add_source(config::File::from(PathBuf::from(DEFAULT_CONFIG_PATH)));
}
}
if let Some(ref config_path) = cli.config {
builder = builder.add_source(config::File::from(config_path.clone()));
}
builder = builder.add_source(config::Environment::with_prefix("HYPRSTREAM"));
if let Some(ref host) = cli.host {
builder = builder.set_override("server.host", host.as_str())?;
}
if let Some(port) = cli.port {
builder = builder.set_override("server.port", port)?;
}
if let Some(ref log_level) = cli.log_level {
builder = builder.set_override("server.log_level", log_level.as_str())?;
}
if let Some(ref engine) = cli.engine {
builder = builder.set_override("engine.engine", engine.as_str())?;
}
if let Some(ref connection) = cli.engine_connection {
builder = builder.set_override("engine.connection", connection.as_str())?;
}
if let Some(ref options) = cli.engine_options {
let options: std::collections::HashMap<String, String> = options
.iter()
.filter_map(|opt| {
let parts: Vec<&str> = opt.split('=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect();
builder = builder.set_override("engine.options", options)?;
}
if let Some(enabled) = cli.enable_cache {
builder = builder.set_override("cache.enabled", enabled)?;
}
if let Some(ref engine) = cli.cache_engine {
builder = builder.set_override("cache.engine", engine.as_str())?;
}
if let Some(ref connection) = cli.cache_connection {
builder = builder.set_override("cache.connection", connection.as_str())?;
}
if let Some(ref options) = cli.cache_options {
let options: std::collections::HashMap<String, String> = options
.iter()
.filter_map(|opt| {
let parts: Vec<&str> = opt.split('=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect();
builder = builder.set_override("cache.options", options)?;
}
if let Some(duration) = cli.cache_max_duration {
builder = builder.set_override("cache.max_duration_secs", duration)?;
}
let mut settings: Settings = builder.build()?.try_deserialize()?;
settings.engine.credentials = Self::load_engine_credentials(&cli);
settings.cache.credentials = Self::load_cache_credentials(&cli);
Ok(settings)
}
fn load_engine_credentials(cli: &CliArgs) -> Option<Credentials> {
if let (Some(username), Some(password)) = (
env::var("HYPRSTREAM_ENGINE_USERNAME").ok(),
env::var("HYPRSTREAM_ENGINE_PASSWORD").ok(),
) {
return Some(Credentials { username, password });
}
if let (Some(username), Some(password)) = (&cli.engine_username, &cli.engine_password) {
return Some(Credentials {
username: username.clone(),
password: password.clone(),
});
}
None
}
fn load_cache_credentials(cli: &CliArgs) -> Option<Credentials> {
if let (Some(username), Some(password)) = (
env::var("HYPRSTREAM_CACHE_USERNAME").ok(),
env::var("HYPRSTREAM_CACHE_PASSWORD").ok(),
) {
return Some(Credentials { username, password });
}
if let (Some(username), Some(password)) = (&cli.cache_username, &cli.cache_password) {
return Some(Credentials {
username: username.clone(),
password: password.clone(),
});
}
None
}
}