1use std::path::{Path, PathBuf};
18
19pub const DEFAULT_DAEMON_PORT: u16 = 60887;
21
22pub const DEFAULT_BIND_HOST: &str = "127.0.0.1";
24
25const BOOTSTRAP_RELATIVE_PATH: &str = ".gobby/bootstrap.yaml";
26
27#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DaemonEndpoint {
34 pub host: String,
35 pub port: u16,
36}
37
38impl Default for DaemonEndpoint {
39 fn default() -> Self {
40 Self {
41 host: DEFAULT_BIND_HOST.to_string(),
42 port: DEFAULT_DAEMON_PORT,
43 }
44 }
45}
46
47pub fn bootstrap_path() -> Option<PathBuf> {
51 dirs::home_dir().map(|h| h.join(BOOTSTRAP_RELATIVE_PATH))
52}
53
54pub fn read_daemon_endpoint() -> DaemonEndpoint {
59 match bootstrap_path() {
60 Some(path) => read_daemon_endpoint_at(&path),
61 None => DaemonEndpoint::default(),
62 }
63}
64
65pub fn read_daemon_endpoint_at(path: &Path) -> DaemonEndpoint {
70 let Ok(contents) = std::fs::read_to_string(path) else {
71 return DaemonEndpoint::default();
72 };
73 let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(&contents) else {
74 return DaemonEndpoint::default();
75 };
76
77 let port = yaml
78 .get("daemon_port")
79 .and_then(|v| v.as_u64())
80 .and_then(|n| u16::try_from(n).ok())
81 .unwrap_or(DEFAULT_DAEMON_PORT);
82
83 let host = yaml
84 .get("bind_host")
85 .and_then(|v| v.as_str())
86 .map(str::to_owned)
87 .unwrap_or_else(|| DEFAULT_BIND_HOST.to_string());
88
89 DaemonEndpoint { host, port }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use std::fs;
96 use tempfile::tempdir;
97
98 #[test]
99 fn missing_file_returns_defaults() {
100 let dir = tempdir().unwrap();
101 let path = dir.path().join("does-not-exist.yaml");
102 assert_eq!(read_daemon_endpoint_at(&path), DaemonEndpoint::default());
103 }
104
105 #[test]
106 fn malformed_yaml_returns_defaults() {
107 let dir = tempdir().unwrap();
108 let path = dir.path().join("bootstrap.yaml");
109 fs::write(&path, ": : not valid yaml ::\n\t").unwrap();
110 assert_eq!(read_daemon_endpoint_at(&path), DaemonEndpoint::default());
111 }
112
113 #[test]
114 fn empty_file_returns_defaults() {
115 let dir = tempdir().unwrap();
116 let path = dir.path().join("bootstrap.yaml");
117 fs::write(&path, "").unwrap();
118 assert_eq!(read_daemon_endpoint_at(&path), DaemonEndpoint::default());
119 }
120
121 #[test]
122 fn missing_fields_return_defaults() {
123 let dir = tempdir().unwrap();
124 let path = dir.path().join("bootstrap.yaml");
125 fs::write(&path, "other_field: value\n").unwrap();
126 assert_eq!(read_daemon_endpoint_at(&path), DaemonEndpoint::default());
127 }
128
129 #[test]
130 fn reads_custom_port() {
131 let dir = tempdir().unwrap();
132 let path = dir.path().join("bootstrap.yaml");
133 fs::write(&path, "daemon_port: 61234\n").unwrap();
134 let ep = read_daemon_endpoint_at(&path);
135 assert_eq!(ep.port, 61234);
136 assert_eq!(ep.host, DEFAULT_BIND_HOST);
137 }
138
139 #[test]
140 fn reads_custom_host_and_port() {
141 let dir = tempdir().unwrap();
142 let path = dir.path().join("bootstrap.yaml");
143 fs::write(&path, "daemon_port: 60887\nbind_host: 0.0.0.0\n").unwrap();
144 let ep = read_daemon_endpoint_at(&path);
145 assert_eq!(ep.port, 60887);
146 assert_eq!(ep.host, "0.0.0.0");
147 }
148
149 #[test]
150 fn out_of_range_port_falls_back() {
151 let dir = tempdir().unwrap();
152 let path = dir.path().join("bootstrap.yaml");
153 fs::write(&path, "daemon_port: 70000\n").unwrap();
154 assert_eq!(read_daemon_endpoint_at(&path).port, DEFAULT_DAEMON_PORT);
155 }
156}