1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub struct HeadlessConfig {
8 #[serde(default)]
9 pub bootstrap_peers: Vec<String>,
10 #[serde(default = "default_listen_addresses")]
11 pub listen_addresses: Vec<String>,
12 #[serde(default)]
13 pub rooms: Vec<String>,
14 #[serde(default = "default_max_connections")]
15 pub max_connections: u32,
16 #[serde(default = "default_api_bind")]
17 pub api_bind: String,
18 #[serde(default = "default_data_dir")]
19 pub data_dir: PathBuf,
20 #[serde(default)]
21 pub relay_server: bool,
22}
23
24fn default_listen_addresses() -> Vec<String> {
25 vec!["/ip4/0.0.0.0/tcp/0".into()]
26}
27
28fn default_max_connections() -> u32 {
29 100
30}
31
32fn default_api_bind() -> String {
33 "127.0.0.1:3000".into()
34}
35
36fn default_data_dir() -> PathBuf {
37 PathBuf::from(".")
38}
39
40impl Default for HeadlessConfig {
41 fn default() -> Self {
42 Self {
43 bootstrap_peers: Vec::new(),
44 listen_addresses: default_listen_addresses(),
45 rooms: Vec::new(),
46 max_connections: default_max_connections(),
47 api_bind: default_api_bind(),
48 data_dir: default_data_dir(),
49 relay_server: false,
50 }
51 }
52}
53
54pub fn load_config(path: &std::path::Path) -> anyhow::Result<HeadlessConfig> {
55 let content = std::fs::read_to_string(path)?;
56 let config: HeadlessConfig = toml::from_str(&content)?;
57 Ok(config)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn default_config_has_sane_defaults() {
66 let cfg = HeadlessConfig::default();
67 assert_eq!(cfg.max_connections, 100);
68 assert_eq!(cfg.api_bind, "127.0.0.1:3000");
69 assert!(cfg.listen_addresses.contains(&"/ip4/0.0.0.0/tcp/0".into()));
70 assert_eq!(cfg.data_dir, PathBuf::from("."));
71 assert!(!cfg.relay_server);
72 assert!(cfg.bootstrap_peers.is_empty());
73 assert!(cfg.rooms.is_empty());
74 }
75
76 #[test]
77 fn parse_full_toml_config() {
78 let toml_str = r#"
79bootstrap_peers = ["/dns4/example.com/tcp/443/wss/p2p/12D3KooWGz9xtYbQ8UYj4oRmx4pD3vZvq6T5zN4pMjz6p6V7X8Y9"]
80listen_addresses = ["/ip4/127.0.0.1/tcp/4001"]
81rooms = ["general", "rust"]
82max_connections = 50
83api_bind = "0.0.0.0:8080"
84data_dir = "/var/lib/chat"
85relay_server = true
86"#;
87 let cfg: HeadlessConfig = toml::from_str(toml_str).unwrap();
88 assert_eq!(cfg.bootstrap_peers.len(), 1);
89 assert_eq!(cfg.listen_addresses, vec!["/ip4/127.0.0.1/tcp/4001"]);
90 assert_eq!(cfg.rooms, vec!["general", "rust"]);
91 assert_eq!(cfg.max_connections, 50);
92 assert_eq!(cfg.api_bind, "0.0.0.0:8080");
93 assert_eq!(cfg.data_dir, PathBuf::from("/var/lib/chat"));
94 assert!(cfg.relay_server);
95 }
96
97 #[test]
98 fn parse_minimal_toml_config() {
99 let toml_str = r#"
100rooms = ["test"]
101"#;
102 let cfg: HeadlessConfig = toml::from_str(toml_str).unwrap();
103 assert_eq!(cfg.rooms, vec!["test"]);
104 assert_eq!(cfg.max_connections, 100); assert_eq!(cfg.api_bind, "127.0.0.1:3000");
106 }
107
108 #[test]
109 fn load_config_from_file() {
110 let dir = std::env::temp_dir();
111 let path = dir.join("chat-headless-test-config.toml");
112 std::fs::write(
113 &path,
114 r#"rooms = ["file-room"]
115max_connections = 42
116"#,
117 )
118 .unwrap();
119 let cfg = load_config(&path).unwrap();
120 assert_eq!(cfg.rooms, vec!["file-room"]);
121 assert_eq!(cfg.max_connections, 42);
122 std::fs::remove_file(&path).unwrap();
123 }
124}