Skip to main content

canlink_hal/
config.rs

1//! Configuration management for backends.
2//!
3//! This module provides types for loading and managing backend configuration
4//! from TOML files.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10/// Backend configuration.
11///
12/// Contains the backend name and backend-specific parameters loaded from
13/// a TOML configuration file.
14///
15/// # Examples
16///
17/// ```
18/// use canlink_hal::BackendConfig;
19///
20/// let config = BackendConfig {
21///     backend_name: "mock".to_string(),
22///     retry_count: Some(3),
23///     retry_interval_ms: Some(1000),
24///     parameters: std::collections::HashMap::new(),
25/// };
26///
27/// assert_eq!(config.backend_name, "mock");
28/// ```
29#[derive(Debug, Clone, Deserialize, Serialize)]
30pub struct BackendConfig {
31    /// Backend name (e.g., "tsmaster", "mock", "peak")
32    pub backend_name: String,
33
34    /// Number of initialization retry attempts (default: 3)
35    #[serde(default = "default_retry_count")]
36    pub retry_count: Option<u32>,
37
38    /// Retry interval in milliseconds (default: 1000)
39    #[serde(default = "default_retry_interval")]
40    pub retry_interval_ms: Option<u64>,
41
42    /// Backend-specific parameters
43    #[serde(flatten)]
44    pub parameters: HashMap<String, toml::Value>,
45}
46
47#[allow(clippy::unnecessary_wraps)]
48fn default_retry_count() -> Option<u32> {
49    Some(3)
50}
51
52#[allow(clippy::unnecessary_wraps)]
53fn default_retry_interval() -> Option<u64> {
54    Some(1000)
55}
56
57impl BackendConfig {
58    /// Create a new backend configuration.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use canlink_hal::BackendConfig;
64    ///
65    /// let config = BackendConfig::new("mock");
66    /// assert_eq!(config.backend_name, "mock");
67    /// ```
68    #[must_use]
69    pub fn new(backend_name: impl Into<String>) -> Self {
70        Self {
71            backend_name: backend_name.into(),
72            retry_count: Some(3),
73            retry_interval_ms: Some(1000),
74            parameters: HashMap::new(),
75        }
76    }
77
78    /// Get a parameter value.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use canlink_hal::BackendConfig;
84    ///
85    /// let mut config = BackendConfig::new("mock");
86    /// config.parameters.insert("device_index".to_string(), toml::Value::Integer(0));
87    ///
88    /// let value = config.get_parameter("device_index");
89    /// assert!(value.is_some());
90    /// ```
91    #[must_use]
92    pub fn get_parameter(&self, key: &str) -> Option<&toml::Value> {
93        self.parameters.get(key)
94    }
95
96    /// Get a parameter as an integer.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use canlink_hal::BackendConfig;
102    ///
103    /// let mut config = BackendConfig::new("mock");
104    /// config.parameters.insert("device_index".to_string(), toml::Value::Integer(0));
105    ///
106    /// assert_eq!(config.get_int("device_index"), Some(0));
107    /// ```
108    #[must_use]
109    pub fn get_int(&self, key: &str) -> Option<i64> {
110        self.parameters.get(key)?.as_integer()
111    }
112
113    /// Get a parameter as a string.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use canlink_hal::BackendConfig;
119    ///
120    /// let mut config = BackendConfig::new("mock");
121    /// config.parameters.insert("device".to_string(), toml::Value::String("can0".to_string()));
122    ///
123    /// assert_eq!(config.get_string("device"), Some("can0"));
124    /// ```
125    #[must_use]
126    pub fn get_string(&self, key: &str) -> Option<&str> {
127        self.parameters.get(key)?.as_str()
128    }
129
130    /// Get a parameter as a boolean.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use canlink_hal::BackendConfig;
136    ///
137    /// let mut config = BackendConfig::new("mock");
138    /// config.parameters.insert("canfd".to_string(), toml::Value::Boolean(true));
139    ///
140    /// assert_eq!(config.get_bool("canfd"), Some(true));
141    /// ```
142    #[must_use]
143    pub fn get_bool(&self, key: &str) -> Option<bool> {
144        self.parameters.get(key)?.as_bool()
145    }
146}
147
148/// Complete configuration file structure.
149///
150/// Represents the top-level structure of a `canlink.toml` configuration file.
151///
152/// # Examples
153///
154/// ```toml
155/// [backend]
156/// backend_name = "mock"
157/// retry_count = 3
158/// retry_interval_ms = 1000
159/// device_index = 0
160/// ```
161#[derive(Debug, Clone, Deserialize, Serialize)]
162pub struct CanlinkConfig {
163    /// Backend configuration
164    pub backend: BackendConfig,
165}
166
167impl CanlinkConfig {
168    /// Load configuration from a TOML file.
169    ///
170    /// # Arguments
171    ///
172    /// * `path` - Path to the TOML configuration file
173    ///
174    /// # Errors
175    ///
176    /// Returns `CanError::ConfigError` if the file cannot be read or parsed.
177    ///
178    /// # Examples
179    ///
180    /// ```no_run
181    /// use canlink_hal::CanlinkConfig;
182    ///
183    /// let config = CanlinkConfig::from_file("canlink.toml").unwrap();
184    /// println!("Backend: {}", config.backend.backend_name);
185    /// ```
186    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, crate::error::CanError> {
187        let path = path.as_ref();
188        let content =
189            std::fs::read_to_string(path).map_err(|e| crate::error::CanError::ConfigError {
190                reason: format!("Failed to read config file '{}': {}", path.display(), e),
191            })?;
192
193        Self::parse_toml(&content)
194    }
195
196    /// Parse configuration from a TOML string.
197    ///
198    /// # Errors
199    ///
200    /// Returns `CanError::ConfigError` if the TOML is invalid.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use canlink_hal::CanlinkConfig;
206    ///
207    /// let toml = r#"
208    /// [backend]
209    /// backend_name = "mock"
210    /// "#;
211    ///
212    /// let config = CanlinkConfig::parse_toml(toml).unwrap();
213    /// assert_eq!(config.backend.backend_name, "mock");
214    /// ```
215    pub fn parse_toml(s: &str) -> Result<Self, crate::error::CanError> {
216        toml::from_str(s).map_err(|e| crate::error::CanError::ConfigError {
217            reason: format!("Failed to parse TOML config: {e}"),
218        })
219    }
220
221    /// Create a default configuration with the specified backend.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use canlink_hal::CanlinkConfig;
227    ///
228    /// let config = CanlinkConfig::with_backend("mock");
229    /// assert_eq!(config.backend.backend_name, "mock");
230    /// ```
231    #[must_use]
232    pub fn with_backend(backend_name: impl Into<String>) -> Self {
233        Self {
234            backend: BackendConfig::new(backend_name),
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_backend_config_new() {
245        let config = BackendConfig::new("mock");
246        assert_eq!(config.backend_name, "mock");
247        assert_eq!(config.retry_count, Some(3));
248        assert_eq!(config.retry_interval_ms, Some(1000));
249    }
250
251    #[test]
252    fn test_backend_config_parameters() {
253        let mut config = BackendConfig::new("mock");
254        config
255            .parameters
256            .insert("device_index".to_string(), toml::Value::Integer(0));
257        config
258            .parameters
259            .insert("canfd".to_string(), toml::Value::Boolean(true));
260        config.parameters.insert(
261            "device".to_string(),
262            toml::Value::String("can0".to_string()),
263        );
264
265        assert_eq!(config.get_int("device_index"), Some(0));
266        assert_eq!(config.get_bool("canfd"), Some(true));
267        assert_eq!(config.get_string("device"), Some("can0"));
268    }
269
270    #[test]
271    fn test_canlink_config_from_str() {
272        let toml = r#"
273[backend]
274backend_name = "mock"
275retry_count = 5
276retry_interval_ms = 2000
277device_index = 1
278"#;
279
280        let config = CanlinkConfig::parse_toml(toml).unwrap();
281        assert_eq!(config.backend.backend_name, "mock");
282        assert_eq!(config.backend.retry_count, Some(5));
283        assert_eq!(config.backend.retry_interval_ms, Some(2000));
284        assert_eq!(config.backend.get_int("device_index"), Some(1));
285    }
286
287    #[test]
288    fn test_canlink_config_with_backend() {
289        let config = CanlinkConfig::with_backend("mock");
290        assert_eq!(config.backend.backend_name, "mock");
291    }
292
293    #[test]
294    fn test_invalid_toml() {
295        let toml = "invalid toml {{{";
296        assert!(CanlinkConfig::parse_toml(toml).is_err());
297    }
298}