feat: config types and loader

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 03:49:25 +00:00
co-authored by Claude Sonnet 4.6
parent 08b7f94e4c
commit 8ab3bb2d12
2 changed files with 143 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NodeRole { Primary, Secondary }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NodeConfig {
pub name: String,
pub role: NodeRole,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HotConfig {
pub path: PathBuf,
pub max_gb: u64,
pub stale_hours: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WarmConfig {
pub projects_path: PathBuf,
pub zfs_dataset: String,
pub snapshot_retain_hours: u64,
pub snapshot_retain_days: u64,
pub snapshot_retain_weeks: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ColdConfig {
pub archive_path: PathBuf,
pub zfs_dataset: String,
pub retain_weeks: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReplicationConfig {
pub receive_from_peer: Option<bool>,
pub peer_user: Option<String>,
pub send_to_host: Option<String>,
pub send_to_user: Option<String>,
pub cold_dataset_on_peer: Option<String>,
pub nightly_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub node: NodeConfig,
pub hot: HotConfig,
pub warm: WarmConfig,
pub cold: Option<ColdConfig>,
pub replication: Option<ReplicationConfig>,
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("reading config at {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("parsing config at {}", path.display()))
}
pub fn default_path() -> PathBuf {
PathBuf::from("/etc/claw-store/config.toml")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_primary_config() {
let toml = r#"
[node]
name = "architect"
role = "primary"
[hot]
path = "/hot/targets"
max_gb = 200
stale_hours = 48
[warm]
projects_path = "/slab/projects"
zfs_dataset = "slab/projects"
snapshot_retain_hours = 24
snapshot_retain_days = 7
snapshot_retain_weeks = 4
[cold]
archive_path = "/data/archive"
zfs_dataset = "data/archive"
retain_weeks = 12
[replication]
receive_from_peer = true
peer_user = "osobh"
"#;
let cfg: Config = toml::from_str(toml).unwrap();
assert_eq!(cfg.node.name, "architect");
assert_eq!(cfg.node.role, NodeRole::Primary);
assert_eq!(cfg.hot.max_gb, 200);
assert!(cfg.cold.is_some());
assert_eq!(cfg.cold.unwrap().retain_weeks, 12);
}
#[test]
fn test_load_secondary_config() {
let toml = r#"
[node]
name = "tank"
role = "secondary"
[hot]
path = "/hot/targets"
max_gb = 300
stale_hours = 48
[warm]
projects_path = "/slab/projects"
zfs_dataset = "slab/projects"
snapshot_retain_hours = 24
snapshot_retain_days = 7
snapshot_retain_weeks = 4
[replication]
send_to_host = "10.10.0.9"
send_to_user = "osobh"
cold_dataset_on_peer = "data/archive/tank-projects"
nightly_at = "03:30"
"#;
let cfg: Config = toml::from_str(toml).unwrap();
assert_eq!(cfg.node.role, NodeRole::Secondary);
assert!(cfg.cold.is_none());
let rep = cfg.replication.unwrap();
assert_eq!(rep.send_to_host.unwrap(), "10.10.0.9");
}
}