1use std::net::IpAddr;
2use std::path::PathBuf;
3
4use koi_common::firewall::{FirewallPort, FirewallProtocol};
5use koi_dns::DnsConfig;
6use koi_runtime::RuntimeBackendKind;
7
8#[derive(Debug, Clone)]
9pub struct KoiConfig {
10 pub data_dir: Option<PathBuf>,
11 pub service_endpoint: String,
12 pub service_mode: ServiceMode,
13 pub http_enabled: bool,
14 pub mdns_enabled: bool,
15 pub dns_enabled: bool,
16 pub health_enabled: bool,
17 pub certmesh_enabled: bool,
18 pub proxy_enabled: bool,
19 pub udp_enabled: bool,
20 pub runtime_enabled: bool,
21 pub runtime_backend: RuntimeBackendKind,
22 pub http_port: u16,
23 pub dashboard_enabled: bool,
24 pub api_docs_enabled: bool,
25 pub mdns_browser_enabled: bool,
26 pub announce_http: bool,
27 pub dns_config: DnsConfig,
28 pub dns_auto_start: bool,
29 pub health_auto_start: bool,
30 pub proxy_auto_start: bool,
31}
32
33impl KoiConfig {
34 pub fn firewall_ports(&self) -> Vec<FirewallPort> {
39 use std::collections::HashSet;
40
41 let mut ports = Vec::new();
42 if self.mdns_enabled {
43 ports.extend(koi_mdns::firewall_ports());
44 }
45 if self.http_enabled {
46 ports.push(FirewallPort::new(
47 "HTTP",
48 FirewallProtocol::Tcp,
49 self.http_port,
50 ));
51 }
52 if self.dns_enabled {
53 ports.extend(koi_dns::firewall_ports(&self.dns_config));
54 }
55
56 let mut seen = HashSet::new();
58 ports
59 .into_iter()
60 .filter(|p| seen.insert((p.protocol, p.port)))
61 .collect()
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ServiceMode {
67 Auto,
68 EmbeddedOnly,
69 ClientOnly,
70}
71
72impl Default for KoiConfig {
73 fn default() -> Self {
74 Self {
75 data_dir: None,
76 service_endpoint: "http://127.0.0.1:5641".to_string(),
77 service_mode: ServiceMode::Auto,
78 http_enabled: false,
79 mdns_enabled: true,
80 dns_enabled: true,
81 health_enabled: false,
82 certmesh_enabled: false,
83 proxy_enabled: false,
84 udp_enabled: false,
85 runtime_enabled: false,
86 runtime_backend: RuntimeBackendKind::Auto,
87 http_port: 5641,
88 dashboard_enabled: false,
89 api_docs_enabled: false,
90 mdns_browser_enabled: false,
91 announce_http: false,
92 dns_config: DnsConfig::default(),
93 dns_auto_start: false,
94 health_auto_start: false,
95 proxy_auto_start: false,
96 }
97 }
98}
99
100pub struct DnsConfigBuilder {
101 config: DnsConfig,
102}
103
104impl DnsConfigBuilder {
105 pub fn new(config: DnsConfig) -> Self {
106 Self { config }
107 }
108
109 pub fn bind_addr(mut self, addr: IpAddr) -> Self {
110 self.config.bind_addr = addr;
111 self
112 }
113
114 pub fn port(mut self, port: u16) -> Self {
115 self.config.port = port;
116 self
117 }
118
119 pub fn zone(mut self, zone: impl Into<String>) -> Self {
120 self.config.zone = zone.into();
121 self
122 }
123
124 pub fn local_ttl(mut self, ttl: u32) -> Self {
125 self.config.local_ttl = ttl;
126 self
127 }
128
129 pub fn allow_public_clients(mut self, allow: bool) -> Self {
130 self.config.allow_public_clients = allow;
131 self
132 }
133
134 pub fn max_qps(mut self, max_qps: u32) -> Self {
135 self.config.max_qps = max_qps;
136 self
137 }
138
139 pub fn local_zone(mut self, enabled: bool) -> Self {
140 self.config.local_zone = enabled;
141 self
142 }
143
144 pub fn build(self) -> DnsConfig {
145 self.config
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::net::{IpAddr, Ipv4Addr};
153
154 #[test]
157 fn default_config_has_expected_values() {
158 let cfg = KoiConfig::default();
159 assert_eq!(cfg.service_endpoint, "http://127.0.0.1:5641");
160 assert_eq!(cfg.service_mode, ServiceMode::Auto);
161 assert!(!cfg.http_enabled);
162 assert!(cfg.mdns_enabled);
163 assert!(cfg.dns_enabled);
164 assert!(!cfg.health_enabled);
165 assert!(!cfg.certmesh_enabled);
166 assert!(!cfg.proxy_enabled);
167 assert!(!cfg.udp_enabled);
168 assert!(!cfg.runtime_enabled);
169 assert_eq!(cfg.runtime_backend, RuntimeBackendKind::Auto);
170 assert_eq!(cfg.http_port, 5641);
171 assert!(!cfg.dashboard_enabled);
172 assert!(!cfg.api_docs_enabled);
173 assert!(!cfg.mdns_browser_enabled);
174 assert!(!cfg.announce_http);
175 assert!(!cfg.dns_auto_start);
176 assert!(!cfg.health_auto_start);
177 assert!(!cfg.proxy_auto_start);
178 assert!(cfg.data_dir.is_none());
179 }
180
181 #[test]
182 fn default_config_clone_is_equal() {
183 let cfg = KoiConfig::default();
184 let cloned = cfg.clone();
185 assert_eq!(cfg.http_port, cloned.http_port);
186 assert_eq!(cfg.mdns_enabled, cloned.mdns_enabled);
187 assert_eq!(cfg.service_endpoint, cloned.service_endpoint);
188 }
189
190 #[test]
191 fn default_config_debug_does_not_panic() {
192 let cfg = KoiConfig::default();
193 let debug = format!("{cfg:?}");
194 assert!(debug.contains("KoiConfig"));
195 }
196
197 #[test]
200 fn firewall_ports_includes_http_when_enabled() {
201 let mut cfg = KoiConfig::default();
202 cfg.http_enabled = true;
203 cfg.mdns_enabled = false;
204 cfg.dns_enabled = false;
205 let ports = cfg.firewall_ports();
206 assert!(
207 ports.iter().any(|p| p.port == 5641),
208 "expected HTTP port 5641 in firewall ports"
209 );
210 }
211
212 #[test]
213 fn firewall_ports_respects_custom_http_port() {
214 let mut cfg = KoiConfig::default();
215 cfg.http_enabled = true;
216 cfg.http_port = 9999;
217 cfg.mdns_enabled = false;
218 cfg.dns_enabled = false;
219 let ports = cfg.firewall_ports();
220 assert!(
221 ports.iter().any(|p| p.port == 9999),
222 "expected custom HTTP port 9999"
223 );
224 assert!(
225 !ports.iter().any(|p| p.port == 5641),
226 "should not have default port when overridden"
227 );
228 }
229
230 #[test]
231 fn firewall_ports_empty_when_all_disabled() {
232 let mut cfg = KoiConfig::default();
233 cfg.http_enabled = false;
234 cfg.mdns_enabled = false;
235 cfg.dns_enabled = false;
236 let ports = cfg.firewall_ports();
237 assert!(ports.is_empty(), "all disabled should yield no ports");
238 }
239
240 #[test]
241 fn firewall_ports_deduplicates() {
242 let mut cfg = KoiConfig::default();
243 cfg.http_enabled = false;
246 cfg.mdns_enabled = true;
247 cfg.dns_enabled = true;
248 let ports = cfg.firewall_ports();
249 let mut seen = std::collections::HashSet::new();
250 for p in &ports {
251 assert!(
252 seen.insert((p.protocol, p.port)),
253 "duplicate firewall port: {:?} {}",
254 p.protocol,
255 p.port
256 );
257 }
258 }
259
260 #[test]
263 fn service_mode_equality() {
264 assert_eq!(ServiceMode::Auto, ServiceMode::Auto);
265 assert_eq!(ServiceMode::EmbeddedOnly, ServiceMode::EmbeddedOnly);
266 assert_eq!(ServiceMode::ClientOnly, ServiceMode::ClientOnly);
267 assert_ne!(ServiceMode::Auto, ServiceMode::EmbeddedOnly);
268 assert_ne!(ServiceMode::Auto, ServiceMode::ClientOnly);
269 }
270
271 #[test]
272 fn service_mode_is_copy() {
273 let mode = ServiceMode::Auto;
274 let copy = mode;
275 assert_eq!(mode, copy);
276 }
277
278 #[test]
279 fn service_mode_debug() {
280 let debug = format!("{:?}", ServiceMode::EmbeddedOnly);
281 assert!(debug.contains("EmbeddedOnly"));
282 }
283
284 #[test]
287 fn dns_config_builder_defaults_match_dns_config() {
288 let dns_default = DnsConfig::default();
289 let built = DnsConfigBuilder::new(DnsConfig::default()).build();
290 assert_eq!(built.port, dns_default.port);
291 assert_eq!(built.zone, dns_default.zone);
292 assert_eq!(built.local_ttl, dns_default.local_ttl);
293 assert_eq!(built.allow_public_clients, dns_default.allow_public_clients);
294 assert_eq!(built.max_qps, dns_default.max_qps);
295 assert_eq!(built.local_zone, dns_default.local_zone);
296 }
297
298 #[test]
299 fn dns_config_builder_port() {
300 let cfg = DnsConfigBuilder::new(DnsConfig::default())
301 .port(5353)
302 .build();
303 assert_eq!(cfg.port, 5353);
304 }
305
306 #[test]
307 fn dns_config_builder_bind_addr() {
308 let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
309 let cfg = DnsConfigBuilder::new(DnsConfig::default())
310 .bind_addr(addr)
311 .build();
312 assert_eq!(cfg.bind_addr, addr);
313 }
314
315 #[test]
316 fn dns_config_builder_zone() {
317 let cfg = DnsConfigBuilder::new(DnsConfig::default())
318 .zone("home")
319 .build();
320 assert_eq!(cfg.zone, "home");
321 }
322
323 #[test]
324 fn dns_config_builder_local_ttl() {
325 let cfg = DnsConfigBuilder::new(DnsConfig::default())
326 .local_ttl(120)
327 .build();
328 assert_eq!(cfg.local_ttl, 120);
329 }
330
331 #[test]
332 fn dns_config_builder_allow_public_clients() {
333 let cfg = DnsConfigBuilder::new(DnsConfig::default())
334 .allow_public_clients(true)
335 .build();
336 assert!(cfg.allow_public_clients);
337 }
338
339 #[test]
340 fn dns_config_builder_max_qps() {
341 let cfg = DnsConfigBuilder::new(DnsConfig::default())
342 .max_qps(500)
343 .build();
344 assert_eq!(cfg.max_qps, 500);
345 }
346
347 #[test]
348 fn dns_config_builder_local_zone() {
349 let cfg = DnsConfigBuilder::new(DnsConfig::default())
350 .local_zone(false)
351 .build();
352 assert!(!cfg.local_zone);
353 }
354
355 #[test]
356 fn dns_config_builder_chaining() {
357 let cfg = DnsConfigBuilder::new(DnsConfig::default())
358 .port(5353)
359 .zone("office")
360 .local_ttl(300)
361 .allow_public_clients(true)
362 .max_qps(1000)
363 .local_zone(false)
364 .build();
365 assert_eq!(cfg.port, 5353);
366 assert_eq!(cfg.zone, "office");
367 assert_eq!(cfg.local_ttl, 300);
368 assert!(cfg.allow_public_clients);
369 assert_eq!(cfg.max_qps, 1000);
370 assert!(!cfg.local_zone);
371 }
372}