1use canlink_hal::{BackendConfig, CanError, CanResult};
4use serde::Deserialize;
5use std::path::Path;
6
7const DEFAULT_CONFIG_FILE: &str = "canlink-tscan.toml";
8const DEFAULT_USE_DAEMON: bool = true;
9const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 2000;
10const DEFAULT_DISCONNECT_TIMEOUT_MS: u64 = 3000;
11const DEFAULT_RESTART_MAX_RETRIES: u32 = 3;
12const DEFAULT_RECV_TIMEOUT_MS: u64 = 0;
13
14#[derive(Debug, Clone, Deserialize, Default)]
15pub struct FileConfig {
17 pub use_daemon: Option<bool>,
19 pub daemon_path: Option<String>,
21 pub request_timeout_ms: Option<u64>,
23 pub disconnect_timeout_ms: Option<u64>,
25 pub restart_max_retries: Option<u32>,
27 pub recv_timeout_ms: Option<u64>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TscanDaemonConfig {
34 pub use_daemon: bool,
36 pub daemon_path: Option<String>,
38 pub request_timeout_ms: u64,
40 pub disconnect_timeout_ms: u64,
42 pub restart_max_retries: u32,
44 pub recv_timeout_ms: u64,
46}
47
48impl Default for TscanDaemonConfig {
49 fn default() -> Self {
50 Self {
51 use_daemon: DEFAULT_USE_DAEMON,
52 daemon_path: None,
53 request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS,
54 disconnect_timeout_ms: DEFAULT_DISCONNECT_TIMEOUT_MS,
55 restart_max_retries: DEFAULT_RESTART_MAX_RETRIES,
56 recv_timeout_ms: DEFAULT_RECV_TIMEOUT_MS,
57 }
58 }
59}
60
61impl TscanDaemonConfig {
62 pub fn load_file(path: &Path) -> CanResult<Option<FileConfig>> {
64 if !path.exists() {
65 return Ok(None);
66 }
67
68 let text = std::fs::read_to_string(path).map_err(|err| CanError::ConfigError {
69 reason: format!("failed to read '{}': {err}", path.display()),
70 })?;
71 let parsed: FileConfig = toml::from_str(&text).map_err(|err| CanError::ConfigError {
72 reason: format!("failed to parse '{}': {err}", path.display()),
73 })?;
74 Ok(Some(parsed))
75 }
76
77 pub fn resolve(backend: &BackendConfig) -> CanResult<Self> {
79 let mut merged = Self::default();
80
81 if let Some(file_cfg) = Self::load_file(Path::new(DEFAULT_CONFIG_FILE))? {
82 merged.apply_file(&file_cfg);
83 }
84 merged.apply_backend_config(backend)?;
85 Ok(merged)
86 }
87
88 fn apply_file(&mut self, cfg: &FileConfig) {
89 if let Some(value) = cfg.use_daemon {
90 self.use_daemon = value;
91 }
92 if let Some(value) = &cfg.daemon_path {
93 self.daemon_path = Some(value.clone());
94 }
95 if let Some(value) = cfg.request_timeout_ms {
96 self.request_timeout_ms = value;
97 }
98 if let Some(value) = cfg.disconnect_timeout_ms {
99 self.disconnect_timeout_ms = value;
100 }
101 if let Some(value) = cfg.restart_max_retries {
102 self.restart_max_retries = value;
103 }
104 if let Some(value) = cfg.recv_timeout_ms {
105 self.recv_timeout_ms = value;
106 }
107 }
108
109 fn apply_backend_config(&mut self, cfg: &BackendConfig) -> CanResult<()> {
110 if let Some(value) = read_bool(cfg, "use_daemon")? {
111 self.use_daemon = value;
112 }
113 if let Some(value) = read_string(cfg, "daemon_path")? {
114 self.daemon_path = Some(value);
115 }
116 if let Some(value) = read_u64(cfg, "request_timeout_ms")? {
117 self.request_timeout_ms = value;
118 }
119 if let Some(value) = read_u64(cfg, "disconnect_timeout_ms")? {
120 self.disconnect_timeout_ms = value;
121 }
122 if let Some(value) = read_u32(cfg, "restart_max_retries")? {
123 self.restart_max_retries = value;
124 }
125 if let Some(value) = read_u64(cfg, "recv_timeout_ms")? {
126 self.recv_timeout_ms = value;
127 }
128 Ok(())
129 }
130}
131
132fn read_bool(cfg: &BackendConfig, key: &str) -> CanResult<Option<bool>> {
133 match cfg.parameters.get(key) {
134 None => Ok(None),
135 Some(value) => value.as_bool().map(Some).ok_or(CanError::InvalidParameter {
136 parameter: key.to_string(),
137 reason: "expected boolean".to_string(),
138 }),
139 }
140}
141
142fn read_string(cfg: &BackendConfig, key: &str) -> CanResult<Option<String>> {
143 match cfg.parameters.get(key) {
144 None => Ok(None),
145 Some(value) => {
146 value
147 .as_str()
148 .map(|v| Some(v.to_string()))
149 .ok_or(CanError::InvalidParameter {
150 parameter: key.to_string(),
151 reason: "expected string".to_string(),
152 })
153 }
154 }
155}
156
157fn read_u64(cfg: &BackendConfig, key: &str) -> CanResult<Option<u64>> {
158 match cfg.parameters.get(key) {
159 None => Ok(None),
160 Some(value) => {
161 let raw = value.as_integer().ok_or(CanError::InvalidParameter {
162 parameter: key.to_string(),
163 reason: "expected integer".to_string(),
164 })?;
165 if raw < 0 {
166 return Err(CanError::InvalidParameter {
167 parameter: key.to_string(),
168 reason: "must be >= 0".to_string(),
169 });
170 }
171 Ok(Some(raw as u64))
172 }
173 }
174}
175
176fn read_u32(cfg: &BackendConfig, key: &str) -> CanResult<Option<u32>> {
177 let value = read_u64(cfg, key)?;
178 if let Some(v) = value {
179 if v > u32::MAX as u64 {
180 return Err(CanError::InvalidParameter {
181 parameter: key.to_string(),
182 reason: "out of range for u32".to_string(),
183 });
184 }
185 return Ok(Some(v as u32));
186 }
187 Ok(None)
188}