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}