use std::fmt;
use crate::signer::{HmacSigner, MIN_SECRET_BYTES};
use crate::venture::VentureEnv;
pub trait Config: Send + Sync {
fn get(&self, key: &str) -> Option<String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct EmptyConfig;
impl Config for EmptyConfig {
fn get(&self, _key: &str) -> Option<String> {
None
}
}
#[derive(Debug, Default, Clone)]
pub struct ConfigError {
pub problems: Vec<String>,
}
impl ConfigError {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, problem: impl Into<String>) {
self.problems.push(problem.into());
}
pub fn is_empty(&self) -> bool {
self.problems.is_empty()
}
pub fn into_result(self) -> Result<(), Self> {
if self.is_empty() { Ok(()) } else { Err(self) }
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid harness configuration:")?;
for problem in &self.problems {
write!(f, "\n - {problem}")?;
}
Ok(())
}
}
impl std::error::Error for ConfigError {}
pub struct ModuleConfig<'a> {
prefix: String,
config: &'a dyn Config,
}
fn screaming_snake(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 8);
for ch in name.chars() {
if ch == '-' || ch == '_' {
out.push('_');
} else {
out.extend(ch.to_uppercase());
}
}
out
}
impl<'a> ModuleConfig<'a> {
pub fn new(module_name: &str, config: &'a dyn Config) -> Self {
Self {
prefix: screaming_snake(module_name),
config,
}
}
pub fn key(&self, suffix: &str) -> String {
format!("{}_{}", self.prefix, screaming_snake(suffix))
}
pub fn get_str(&self, key_suffix: &str, default: &str) -> String {
self.config
.get(&self.key(key_suffix))
.unwrap_or_else(|| default.to_string())
}
pub fn get_u32(&self, key_suffix: &str, default: u32) -> u32 {
self.config
.get(&self.key(key_suffix))
.and_then(|raw| raw.parse().ok())
.unwrap_or(default)
}
pub fn get_bool(&self, key_suffix: &str, default: bool) -> bool {
match self.config.get(&self.key(key_suffix)) {
Some(raw) => matches!(
raw.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
),
None => default,
}
}
pub fn get_opt(&self, key_suffix: &str) -> Option<String> {
self.config.get(&self.key(key_suffix))
}
}
#[derive(Debug, Clone)]
pub struct HarnessConfig {
pub harness_secret: String,
pub harness_secret_previous: Option<String>,
pub admin_token: Option<String>,
pub env: VentureEnv,
}
impl HarnessConfig {
pub fn from_config(config: &dyn Config) -> Result<Self, ConfigError> {
let mut errors = ConfigError::default();
let harness_secret = match config.get("HARNESS_SECRET") {
Some(secret) if secret.len() >= MIN_SECRET_BYTES => Some(secret),
Some(_) => {
errors.push(format!(
"HARNESS_SECRET must be at least {MIN_SECRET_BYTES} bytes"
));
None
}
None => {
errors.push(format!(
"HARNESS_SECRET is required (min {MIN_SECRET_BYTES} bytes)"
));
None
}
};
let env = match config.get("ENV").as_deref() {
None | Some("") => Some(VentureEnv::Development),
Some(raw) => {
let parsed = VentureEnv::parse(raw);
if parsed.is_none() {
errors.push(format!(
"ENV must be one of development|staging|production, got {raw:?}"
));
}
parsed
}
};
errors.into_result()?;
Ok(Self {
harness_secret: harness_secret.unwrap_or_default(),
harness_secret_previous: config.get("HARNESS_SECRET_PREVIOUS"),
admin_token: config.get("ADMIN_TOKEN"),
env: env.unwrap_or_default(),
})
}
pub fn signer(&self) -> HmacSigner {
HmacSigner::new(
self.harness_secret.clone(),
self.harness_secret_previous.clone(),
)
.expect("from_config validated the secret")
}
}
#[derive(Debug, Clone, Default)]
pub struct MapConfig(pub std::collections::HashMap<String, String>);
impl MapConfig {
pub fn from_pairs(
pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Self {
Self(
pairs
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
impl Config for MapConfig {
fn get(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}