use serde::{Deserialize, Serialize};
use shikumi::TieredConfig;
use crate::error::ConfigError;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClusterConfig {
pub name: String,
pub region: String,
}
impl TieredConfig for ClusterConfig {
fn bare() -> Self {
Self {
name: String::new(),
region: String::new(),
}
}
fn prescribed_default() -> Self {
Self {
name: "engenho-local".into(),
region: "homelab".into(),
}
}
fn extend(self, base: &Self) -> Self {
Self {
name: if self.name.is_empty() {
base.name.clone()
} else {
self.name
},
region: if self.region.is_empty() {
base.region.clone()
} else {
self.region
},
}
}
}
impl ClusterConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.name.is_empty() {
return Err(ConfigError::InvalidField {
field: "cluster.name".into(),
reason: "cluster name cannot be empty".into(),
});
}
if self.name.contains('.') || self.name.contains(' ') {
return Err(ConfigError::InvalidField {
field: "cluster.name".into(),
reason: format!("cluster name '{}' contains invalid chars (.,space)", self.name),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prescribed_default_validates() {
ClusterConfig::prescribed_default().validate().unwrap();
}
#[test]
fn bare_fails_validation() {
assert!(ClusterConfig::bare().validate().is_err());
}
#[test]
fn name_with_dot_rejected() {
let cfg = ClusterConfig {
name: "rio.example".into(),
region: "x".into(),
};
assert!(cfg.validate().is_err());
}
#[test]
fn extend_fills_empty_fields_from_base() {
let overlay = ClusterConfig {
name: "myname".into(),
region: String::new(),
};
let base = ClusterConfig::prescribed_default();
let merged = overlay.extend(&base);
assert_eq!(merged.name, "myname");
assert_eq!(merged.region, "homelab");
}
}