pub mod data_sources;
pub mod env;
pub mod schema;
pub mod slice;
pub mod validate;
pub use schema::*;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file '{path}': {source}")]
ReadFile {
path: String,
source: std::io::Error,
},
#[error("invalid YAML in config file: {0}")]
InvalidYaml(#[from] serde_yaml::Error),
#[error("environment variable error: {0}")]
EnvVar(#[from] env::EnvVarError),
#[error("validation: {0}")]
Validation(String),
#[error("deployment '{0}' not found in config")]
DeploymentNotFound(String),
}
pub fn load_config(path: &Path) -> Result<Config, ConfigError> {
let raw = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
path: path.display().to_string(),
source: e,
})?;
let expanded = env::substitute_env_vars(&raw)?;
let config: Config = serde_yaml::from_str(&expanded)?;
validate::run(&config)?;
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn loads_with_env_substitution() {
unsafe {
std::env::set_var("Q_TEST_SUB", "subA");
}
let yaml = r#"
azure:
subscription_id: "${Q_TEST_SUB}"
resource_group: "rg"
region: "swedencentral"
cosmos:
database: "quelch"
ai:
provider: azure_openai
endpoint: "https://x.openai.azure.com"
embedding:
deployment: "te"
dimensions: 1536
chat:
deployment: "gpt-5-mini"
model_name: "gpt-5-mini"
sources: []
deployments: []
"#;
let mut f = NamedTempFile::new().unwrap();
f.write_all(yaml.as_bytes()).unwrap();
let cfg = load_config(f.path()).unwrap();
assert_eq!(cfg.azure.subscription_id, "subA");
}
#[test]
fn loads_minimal_fixture() {
let path = std::path::Path::new("tests/fixtures/quelch.minimal.yaml");
let raw = std::fs::read_to_string(path).expect("fixture must exist");
let cfg: Config = serde_yaml::from_str(&raw).expect("fixture must parse");
validate::run(&cfg).expect("fixture must be valid");
assert!(!cfg.azure.subscription_id.is_empty());
}
}