1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use koi_common::paths;
8use koi_common::persist;
9
10#[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#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
21pub struct DnsState {
22 #[serde(default)]
23 pub entries: Vec<DnsEntry>,
24}
25
26pub fn dns_state_path() -> PathBuf {
28 paths::koi_state_dir().join("dns.json")
29}
30
31pub fn load_dns_state() -> Result<DnsState, std::io::Error> {
33 let path = dns_state_path();
34 persist::read_json_or_default(&path)
35}
36
37pub 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}