chat_headless/
config.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for the headless chat daemon.
6#[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); // default
105        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}