1use std::env;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use crate::Error;
8
9const DEFAULT_HEALTH_CHECK_TIMEOUT: Duration = Duration::from_secs(2);
11const DEFAULT_REGISTER_TIMEOUT: Duration = Duration::from_secs(5);
13const DEFAULT_UNREGISTER_TIMEOUT: Duration = Duration::from_secs(2);
15const DEFAULT_LLDB_START_TIMEOUT: Duration = Duration::from_secs(10);
17
18#[derive(Debug, Clone)]
20pub struct Config {
21 pub name: Option<String>,
23
24 pub control_host: String,
26
27 pub advertise_host: Option<String>,
32
33 pub control_port: u16,
35
36 pub debug_port: u16,
38
39 pub daemon_url: String,
41
42 pub lldb_dap_path: Option<PathBuf>,
44
45 pub detrix_home: Option<PathBuf>,
47
48 pub workspace_root: Option<String>,
51
52 pub safe_mode: bool,
55
56 pub build_commit: Option<String>,
58
59 pub build_tag: Option<String>,
61
62 pub health_check_timeout: Duration,
64
65 pub register_timeout: Duration,
67
68 pub unregister_timeout: Duration,
70
71 pub lldb_start_timeout: Duration,
73}
74
75impl Default for Config {
76 fn default() -> Self {
77 Self {
78 name: None,
79 control_host: "127.0.0.1".to_string(),
80 advertise_host: None,
81 control_port: 0,
82 debug_port: 0,
83 daemon_url: "http://127.0.0.1:8090".to_string(),
84 lldb_dap_path: None,
85 detrix_home: None,
86 workspace_root: None,
87 safe_mode: false,
88 build_commit: None,
89 build_tag: None,
90 health_check_timeout: DEFAULT_HEALTH_CHECK_TIMEOUT,
91 register_timeout: DEFAULT_REGISTER_TIMEOUT,
93 unregister_timeout: DEFAULT_UNREGISTER_TIMEOUT,
94 lldb_start_timeout: DEFAULT_LLDB_START_TIMEOUT,
95 }
96 }
97}
98
99impl Config {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn with_env_overrides(mut self) -> Self {
110 if self.name.is_none() {
112 if let Ok(name) = env::var("DETRIX_NAME") {
113 if !name.is_empty() {
114 self.name = Some(name);
115 }
116 }
117 }
118
119 if self.control_host == "127.0.0.1" {
121 if let Ok(host) = env::var("DETRIX_CONTROL_HOST") {
122 if !host.is_empty() {
123 self.control_host = host;
124 }
125 }
126 }
127
128 if self.advertise_host.is_none() {
130 if let Ok(host) = env::var("DETRIX_HOST") {
131 if !host.is_empty() {
132 self.advertise_host = Some(host);
133 }
134 }
135 }
136
137 if self.control_port == 0 {
139 if let Ok(port_str) = env::var("DETRIX_CONTROL_PORT") {
140 if let Ok(port) = port_str.parse() {
141 self.control_port = port;
142 }
143 }
144 }
145
146 if self.debug_port == 0 {
148 if let Ok(port_str) = env::var("DETRIX_DEBUG_PORT") {
149 if let Ok(port) = port_str.parse() {
150 self.debug_port = port;
151 }
152 }
153 }
154
155 if self.daemon_url == "http://127.0.0.1:8090" {
157 if let Ok(url) = env::var("DETRIX_DAEMON_URL") {
158 if !url.is_empty() {
159 self.daemon_url = url;
160 }
161 }
162 }
163
164 if self.lldb_dap_path.is_none() {
166 if let Ok(path) = env::var("DETRIX_LLDB_DAP_PATH") {
167 if !path.is_empty() {
168 self.lldb_dap_path = Some(PathBuf::from(path));
169 }
170 }
171 }
172
173 if self.detrix_home.is_none() {
175 if let Ok(home) = env::var("DETRIX_HOME") {
176 if !home.is_empty() {
177 self.detrix_home = Some(PathBuf::from(home));
178 }
179 }
180 }
181
182 if self.workspace_root.is_none() {
184 if let Ok(root) = env::var("DETRIX_WORKSPACE_ROOT") {
185 if !root.is_empty() {
186 self.workspace_root = Some(root);
187 }
188 }
189 }
190
191 if self.health_check_timeout == DEFAULT_HEALTH_CHECK_TIMEOUT {
193 if let Some(d) = Self::parse_duration_env("DETRIX_HEALTH_CHECK_TIMEOUT") {
194 self.health_check_timeout = d;
195 }
196 }
197 if self.register_timeout == DEFAULT_REGISTER_TIMEOUT {
198 if let Some(d) = Self::parse_duration_env("DETRIX_REGISTER_TIMEOUT") {
199 self.register_timeout = d;
200 }
201 }
202 if self.unregister_timeout == DEFAULT_UNREGISTER_TIMEOUT {
203 if let Some(d) = Self::parse_duration_env("DETRIX_UNREGISTER_TIMEOUT") {
204 self.unregister_timeout = d;
205 }
206 }
207
208 self
209 }
210
211 fn parse_duration_env(key: &str) -> Option<Duration> {
213 env::var(key)
214 .ok()
215 .and_then(|v| v.parse::<f64>().ok())
216 .map(Duration::from_secs_f64)
217 }
218
219 pub fn connection_name(&self) -> String {
223 self.name
224 .clone()
225 .unwrap_or_else(|| format!("detrix-client-{}", std::process::id()))
226 }
227
228 pub fn detrix_home_path(&self) -> Option<PathBuf> {
232 self.detrix_home
233 .clone()
234 .or_else(|| dirs::home_dir().map(|home| home.join("detrix")))
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct TlsConfig {
241 pub verify: bool,
243 pub ca_bundle: Option<PathBuf>,
245}
246
247impl Default for TlsConfig {
248 fn default() -> Self {
249 Self {
250 verify: true,
251 ca_bundle: None,
252 }
253 }
254}
255
256impl TlsConfig {
257 pub fn validate(&self) -> Result<(), Error> {
263 if let Some(ref path) = self.ca_bundle {
264 if !path.is_file() {
265 return Err(Error::ConfigError(format!(
266 "CA bundle not found: {}",
267 path.display()
268 )));
269 }
270 }
271 Ok(())
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_default_config() {
281 let config = Config::default();
282 assert!(config.name.is_none());
283 assert_eq!(config.control_host, "127.0.0.1");
284 assert_eq!(config.control_port, 0);
285 assert_eq!(config.debug_port, 0);
286 assert_eq!(config.daemon_url, "http://127.0.0.1:8090");
287 assert!(config.lldb_dap_path.is_none());
288 assert!(!config.safe_mode);
289 }
290
291 #[test]
292 fn test_register_timeout_default() {
293 let config = Config::default();
294 assert_eq!(config.register_timeout, Duration::from_secs(5));
296 }
297
298 #[test]
299 fn test_connection_name_default() {
300 let config = Config::default();
301 let name = config.connection_name();
302 assert!(name.starts_with("detrix-client-"));
303 }
304
305 #[test]
306 fn test_connection_name_custom() {
307 let config = Config {
308 name: Some("my-service".to_string()),
309 ..Config::default()
310 };
311 assert_eq!(config.connection_name(), "my-service");
312 }
313
314 #[test]
315 fn test_detrix_home_path() {
316 let config = Config::default();
317 if let Some(path) = config.detrix_home_path() {
318 assert!(path.ends_with("detrix"));
319 }
320 }
321
322 #[test]
323 fn test_tls_config_default() {
324 let config = TlsConfig::default();
325 assert!(config.verify);
326 assert!(config.ca_bundle.is_none());
327 }
328
329 #[test]
330 fn test_tls_config_validate_missing_ca_bundle() {
331 let config = TlsConfig {
332 verify: true,
333 ca_bundle: Some(PathBuf::from("/nonexistent/ca.pem")),
334 };
335 assert!(config.validate().is_err());
336 }
337
338 #[test]
339 fn test_tls_config_validate_no_ca_bundle() {
340 let config = TlsConfig::default();
341 assert!(config.validate().is_ok());
342 }
343}