use canlink_hal::{BackendConfig, CanError, CanResult};
use serde::Deserialize;
use std::path::Path;
const DEFAULT_CONFIG_FILE: &str = "canlink-tscan.toml";
const DEFAULT_USE_DAEMON: bool = true;
const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 2000;
const DEFAULT_DISCONNECT_TIMEOUT_MS: u64 = 3000;
const DEFAULT_RESTART_MAX_RETRIES: u32 = 3;
const DEFAULT_RECV_TIMEOUT_MS: u64 = 0;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FileConfig {
pub use_daemon: Option<bool>,
pub daemon_path: Option<String>,
pub request_timeout_ms: Option<u64>,
pub disconnect_timeout_ms: Option<u64>,
pub restart_max_retries: Option<u32>,
pub recv_timeout_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TscanDaemonConfig {
pub use_daemon: bool,
pub daemon_path: Option<String>,
pub request_timeout_ms: u64,
pub disconnect_timeout_ms: u64,
pub restart_max_retries: u32,
pub recv_timeout_ms: u64,
}
impl Default for TscanDaemonConfig {
fn default() -> Self {
Self {
use_daemon: DEFAULT_USE_DAEMON,
daemon_path: None,
request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS,
disconnect_timeout_ms: DEFAULT_DISCONNECT_TIMEOUT_MS,
restart_max_retries: DEFAULT_RESTART_MAX_RETRIES,
recv_timeout_ms: DEFAULT_RECV_TIMEOUT_MS,
}
}
}
impl TscanDaemonConfig {
pub fn load_file(path: &Path) -> CanResult<Option<FileConfig>> {
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(path).map_err(|err| CanError::ConfigError {
reason: format!("failed to read '{}': {err}", path.display()),
})?;
let parsed: FileConfig = toml::from_str(&text).map_err(|err| CanError::ConfigError {
reason: format!("failed to parse '{}': {err}", path.display()),
})?;
Ok(Some(parsed))
}
pub fn resolve(backend: &BackendConfig) -> CanResult<Self> {
let mut merged = Self::default();
if let Some(file_cfg) = Self::load_file(Path::new(DEFAULT_CONFIG_FILE))? {
merged.apply_file(&file_cfg);
}
merged.apply_backend_config(backend)?;
Ok(merged)
}
fn apply_file(&mut self, cfg: &FileConfig) {
if let Some(value) = cfg.use_daemon {
self.use_daemon = value;
}
if let Some(value) = &cfg.daemon_path {
self.daemon_path = Some(value.clone());
}
if let Some(value) = cfg.request_timeout_ms {
self.request_timeout_ms = value;
}
if let Some(value) = cfg.disconnect_timeout_ms {
self.disconnect_timeout_ms = value;
}
if let Some(value) = cfg.restart_max_retries {
self.restart_max_retries = value;
}
if let Some(value) = cfg.recv_timeout_ms {
self.recv_timeout_ms = value;
}
}
fn apply_backend_config(&mut self, cfg: &BackendConfig) -> CanResult<()> {
if let Some(value) = read_bool(cfg, "use_daemon")? {
self.use_daemon = value;
}
if let Some(value) = read_string(cfg, "daemon_path")? {
self.daemon_path = Some(value);
}
if let Some(value) = read_u64(cfg, "request_timeout_ms")? {
self.request_timeout_ms = value;
}
if let Some(value) = read_u64(cfg, "disconnect_timeout_ms")? {
self.disconnect_timeout_ms = value;
}
if let Some(value) = read_u32(cfg, "restart_max_retries")? {
self.restart_max_retries = value;
}
if let Some(value) = read_u64(cfg, "recv_timeout_ms")? {
self.recv_timeout_ms = value;
}
Ok(())
}
}
fn read_bool(cfg: &BackendConfig, key: &str) -> CanResult<Option<bool>> {
match cfg.parameters.get(key) {
None => Ok(None),
Some(value) => value.as_bool().map(Some).ok_or(CanError::InvalidParameter {
parameter: key.to_string(),
reason: "expected boolean".to_string(),
}),
}
}
fn read_string(cfg: &BackendConfig, key: &str) -> CanResult<Option<String>> {
match cfg.parameters.get(key) {
None => Ok(None),
Some(value) => {
value
.as_str()
.map(|v| Some(v.to_string()))
.ok_or(CanError::InvalidParameter {
parameter: key.to_string(),
reason: "expected string".to_string(),
})
}
}
}
fn read_u64(cfg: &BackendConfig, key: &str) -> CanResult<Option<u64>> {
match cfg.parameters.get(key) {
None => Ok(None),
Some(value) => {
let raw = value.as_integer().ok_or(CanError::InvalidParameter {
parameter: key.to_string(),
reason: "expected integer".to_string(),
})?;
if raw < 0 {
return Err(CanError::InvalidParameter {
parameter: key.to_string(),
reason: "must be >= 0".to_string(),
});
}
Ok(Some(raw as u64))
}
}
}
fn read_u32(cfg: &BackendConfig, key: &str) -> CanResult<Option<u32>> {
let value = read_u64(cfg, key)?;
if let Some(v) = value {
if v > u32::MAX as u64 {
return Err(CanError::InvalidParameter {
parameter: key.to_string(),
reason: "out of range for u32".to_string(),
});
}
return Ok(Some(v as u32));
}
Ok(None)
}