Skip to main content

koi_config/
state.rs

1//! Runtime state file management (Phase 1+).
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use koi_common::paths;
8use koi_common::persist;
9
10/// DNS static entry stored in the local state file.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, utoipa::ToSchema)]
12pub struct DnsEntry {
13    pub name: String,
14    pub ip: String,
15    #[serde(default)]
16    pub ttl: Option<u32>,
17}
18
19/// DNS state persisted on disk.
20#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
21pub struct DnsState {
22    #[serde(default)]
23    pub entries: Vec<DnsEntry>,
24}
25
26/// Path to the DNS state file.
27pub fn dns_state_path() -> PathBuf {
28    paths::koi_state_dir().join("dns.json")
29}
30
31/// Load DNS state from disk. Returns default state if missing.
32pub fn load_dns_state() -> Result<DnsState, std::io::Error> {
33    let path = dns_state_path();
34    persist::read_json_or_default(&path)
35}
36
37/// Save DNS state to disk, creating the state directory if needed.
38pub fn save_dns_state(state: &DnsState) -> Result<(), std::io::Error> {
39    let path = dns_state_path();
40    persist::write_json_pretty(&path, state)
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use std::time::{SystemTime, UNIX_EPOCH};
47
48    fn with_temp_data_dir<F, T>(f: F) -> T
49    where
50        F: FnOnce() -> T,
51    {
52        let nanos = SystemTime::now()
53            .duration_since(UNIX_EPOCH)
54            .unwrap_or_default()
55            .as_nanos();
56        let dir = std::env::temp_dir().join(format!("koi-dns-state-test-{nanos}"));
57        let prev = std::env::var("KOI_DATA_DIR").ok();
58        std::env::set_var("KOI_DATA_DIR", &dir);
59        let result = f();
60        match prev {
61            Some(v) => std::env::set_var("KOI_DATA_DIR", v),
62            None => std::env::remove_var("KOI_DATA_DIR"),
63        }
64        result
65    }
66
67    #[test]
68    fn dns_state_round_trip() {
69        let state = DnsState {
70            entries: vec![DnsEntry {
71                name: "grafana.lan".to_string(),
72                ip: "192.168.1.50".to_string(),
73                ttl: Some(60),
74            }],
75        };
76        let json = serde_json::to_string(&state).unwrap();
77        let parsed: DnsState = serde_json::from_str(&json).unwrap();
78        assert_eq!(state, parsed);
79    }
80
81    #[test]
82    fn load_dns_state_missing_returns_default() {
83        with_temp_data_dir(|| {
84            let state = load_dns_state().unwrap();
85            assert!(state.entries.is_empty());
86        });
87    }
88}