Skip to main content

canlink_tscan/
config.rs

1//! Configuration for the TSCan daemon workaround.
2
3use 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)]
15/// File-based daemon configuration loaded from `canlink-tscan.toml`.
16pub struct FileConfig {
17    /// Whether to enable daemon mode.
18    pub use_daemon: Option<bool>,
19    /// Optional daemon executable path.
20    pub daemon_path: Option<String>,
21    /// Request timeout in milliseconds.
22    pub request_timeout_ms: Option<u64>,
23    /// Disconnect timeout in milliseconds.
24    pub disconnect_timeout_ms: Option<u64>,
25    /// Maximum restart retries after daemon failure.
26    pub restart_max_retries: Option<u32>,
27    /// Receive timeout in milliseconds.
28    pub recv_timeout_ms: Option<u64>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32/// Effective daemon configuration merged from file and backend parameters.
33pub struct TscanDaemonConfig {
34    /// Whether to use daemon mode instead of direct DLL calls.
35    pub use_daemon: bool,
36    /// Optional daemon executable path.
37    pub daemon_path: Option<String>,
38    /// Request timeout in milliseconds.
39    pub request_timeout_ms: u64,
40    /// Disconnect timeout in milliseconds.
41    pub disconnect_timeout_ms: u64,
42    /// Maximum restart retries after daemon failure.
43    pub restart_max_retries: u32,
44    /// Receive timeout in milliseconds.
45    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    /// Loads optional file configuration from `path`.
63    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    /// Resolves final configuration from defaults, config file and backend parameters.
78    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}