use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[allow(dead_code)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub cli: CliConfig,
#[serde(default)]
pub logging: LogConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub api_key: Option<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
api_key: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct CliConfig {
pub default_version: String,
pub output_format: String,
}
impl Default for CliConfig {
fn default() -> Self {
Self {
default_version: "2.5.1".to_string(),
output_format: "text".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct LogConfig {
pub level: String,
pub log_to_file: bool,
pub log_path: Option<PathBuf>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
log_to_file: false,
log_path: None,
}
}
}
#[allow(dead_code)]
pub fn load_config(path: impl AsRef<Path>) -> Result<Config, Box<dyn std::error::Error>> {
let path_ref = path.as_ref();
let content = fs::read_to_string(path_ref)?;
let config: Config = if path_ref.extension().and_then(|s| s.to_str()) == Some("yaml") {
serde_yaml::from_str(&content)?
} else {
toml::from_str(&content)?
};
Ok(config)
}
#[allow(dead_code)]
pub fn apply_env_overrides(config: &mut Config) {
if let Ok(host) = std::env::var("HL7_HOST") {
config.server.host = host;
}
if let Ok(port_str) = std::env::var("HL7_PORT")
&& let Ok(port) = port_str.parse::<u16>()
{
config.server.port = port;
}
if let Ok(api_key) = std::env::var("HL7_API_KEY") {
config.server.api_key = Some(api_key);
}
if let Ok(log_level) = std::env::var("HL7_LOG_LEVEL") {
config.logging.level = log_level;
}
}
#[cfg(test)]
mod tests {
use super::{Config, load_config};
use std::fs;
#[test]
fn config_example_matches_loader_shape() {
let config: Config = toml::from_str(include_str!("../../../config.example.toml"))
.expect("config.example.toml should match Config");
assert_example_config(config);
}
#[test]
fn load_config_reads_toml_file() {
let dir = tempfile::tempdir().expect("tempdir should be created");
let path = dir.path().join("hl7v2.toml");
fs::write(&path, include_str!("../../../config.example.toml"))
.expect("config fixture should be written");
let config = load_config(&path).expect("TOML config should load");
assert_example_config(config);
}
#[test]
fn load_config_reads_yaml_file() {
let dir = tempfile::tempdir().expect("tempdir should be created");
let path = dir.path().join("hl7v2.yaml");
fs::write(
&path,
"server:\n host: 0.0.0.0\n port: 8080\ncli:\n default_version: 2.5.1\n output_format: text\nlogging:\n level: info\n log_to_file: false\n",
)
.expect("config fixture should be written");
let config = load_config(&path).expect("YAML config should load");
assert_example_config(config);
}
fn assert_example_config(config: Config) {
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.server.port, 8080);
assert_eq!(config.cli.default_version, "2.5.1");
assert_eq!(config.cli.output_format, "text");
assert_eq!(config.logging.level, "info");
assert!(!config.logging.log_to_file);
}
}