dbc_rs/dbc/
std.rs

1use super::Dbc;
2use crate::Result;
3use std::fmt::{Display, Formatter, Result as FmtResult};
4use std::path::Path;
5
6impl Dbc {
7    /// Load and parse a DBC file from disk.
8    ///
9    /// # Examples
10    ///
11    /// ```rust,no_run
12    /// use dbc_rs::Dbc;
13    ///
14    /// let dbc = Dbc::from_file("path/to/file.dbc")?;
15    /// println!("Loaded {} messages", dbc.messages().len());
16    /// # Ok::<(), dbc_rs::Error>(())
17    /// ```
18    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
19        let content = std::fs::read_to_string(path.as_ref())?;
20        Self::parse(&content)
21    }
22    /// Serialize this DBC to a DBC format string
23    ///
24    /// # Examples
25    ///
26    /// ```rust,no_run
27    /// use dbc_rs::Dbc;
28    ///
29    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM")?;
30    /// let dbc_string = dbc.to_dbc_string();
31    /// // The string can be written to a file or used elsewhere
32    /// assert!(dbc_string.contains("VERSION"));
33    /// # Ok::<(), dbc_rs::Error>(())
34    /// ```
35    #[must_use = "return value should be used"]
36    pub fn to_dbc_string(&self) -> String {
37        // Pre-allocate with estimated capacity
38        // Estimate: ~50 chars per message + ~100 chars per signal
39        let signal_count: usize = self.messages().iter().map(|m| m.signals().len()).sum();
40        let estimated_capacity = 200 + (self.messages.len() * 50) + (signal_count * 100);
41        let mut result = String::with_capacity(estimated_capacity);
42
43        // VERSION line
44        if let Some(version) = &self.version {
45            result.push_str(&version.to_dbc_string());
46            result.push_str("\n\n");
47        }
48
49        // BS_ line (bit timing) - only output if present
50        if let Some(ref bit_timing) = self.bit_timing {
51            result.push_str(&bit_timing.to_string());
52            result.push_str("\n\n");
53        } else {
54            // Empty BS_: is always required per DBC spec
55            result.push_str("BS_:\n\n");
56        }
57
58        // BU_ line
59        result.push_str(&self.nodes.to_dbc_string());
60        result.push('\n');
61
62        // BO_ and SG_ lines for each message
63        for message in self.messages().iter() {
64            result.push('\n');
65            result.push_str(&message.to_string_full());
66        }
67
68        // CM_ lines (comments section)
69        // General database comment
70        if let Some(comment) = self.comment() {
71            result.push_str("\nCM_ \"");
72            result.push_str(comment);
73            result.push_str("\";\n");
74        }
75
76        // Node comments
77        for node in self.nodes.iter_nodes() {
78            if let Some(comment) = node.comment() {
79                result.push_str("CM_ BU_ ");
80                result.push_str(node.name());
81                result.push_str(" \"");
82                result.push_str(comment);
83                result.push_str("\";\n");
84            }
85        }
86
87        // Message and signal comments
88        for message in self.messages().iter() {
89            if let Some(comment) = message.comment() {
90                result.push_str("CM_ BO_ ");
91                result.push_str(&message.id().to_string());
92                result.push_str(" \"");
93                result.push_str(comment);
94                result.push_str("\";\n");
95            }
96
97            for signal in message.signals().iter() {
98                if let Some(comment) = signal.comment() {
99                    result.push_str("CM_ SG_ ");
100                    result.push_str(&message.id().to_string());
101                    result.push(' ');
102                    result.push_str(signal.name());
103                    result.push_str(" \"");
104                    result.push_str(comment);
105                    result.push_str("\";\n");
106                }
107            }
108        }
109
110        result
111    }
112}
113
114impl Display for Dbc {
115    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
116        write!(f, "{}", self.to_dbc_string())
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::Dbc;
123
124    #[test]
125    fn test_to_dbc_string() {
126        let dbc = Dbc::parse(
127            r#"VERSION "1.0"
128
129BU_: ECM
130
131BO_ 256 Engine : 8 ECM
132 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
133"#,
134        )
135        .unwrap();
136
137        let dbc_string = dbc.to_dbc_string();
138        assert!(dbc_string.contains("VERSION"));
139        assert!(dbc_string.contains("BU_"));
140        assert!(dbc_string.contains("BO_"));
141        assert!(dbc_string.contains("SG_"));
142    }
143
144    #[test]
145    fn test_display() {
146        let dbc = Dbc::parse(
147            r#"VERSION "1.0"
148
149BU_: ECM
150
151BO_ 256 Engine : 8 ECM
152"#,
153        )
154        .unwrap();
155
156        let display_str = format!("{}", dbc);
157        assert!(display_str.contains("VERSION"));
158    }
159
160    #[test]
161    fn test_save_round_trip() {
162        let original = r#"VERSION "1.0"
163
164BU_: ECM TCM
165
166BO_ 256 EngineData : 8 ECM
167 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
168 SG_ Temperature : 16|8@0- (1,-40) [-40|215] "°C" TCM
169
170BO_ 512 BrakeData : 4 TCM
171 SG_ Pressure : 0|16@0+ (0.1,0) [0|1000] "bar"
172"#;
173
174        let dbc = Dbc::parse(original).unwrap();
175        let saved = dbc.to_dbc_string();
176        let dbc2 = Dbc::parse(&saved).unwrap();
177
178        // Verify round-trip: parsed data should match
179        assert_eq!(
180            dbc.version().map(|v| v.to_string()),
181            dbc2.version().map(|v| v.to_string())
182        );
183        assert_eq!(dbc.messages().len(), dbc2.messages().len());
184
185        for (msg1, msg2) in dbc.messages().iter().zip(dbc2.messages().iter()) {
186            assert_eq!(msg1.id(), msg2.id());
187            assert_eq!(msg1.name(), msg2.name());
188            assert_eq!(msg1.dlc(), msg2.dlc());
189            assert_eq!(msg1.sender(), msg2.sender());
190            assert_eq!(msg1.signals().len(), msg2.signals().len());
191
192            for (sig1, sig2) in msg1.signals().iter().zip(msg2.signals().iter()) {
193                assert_eq!(sig1.name(), sig2.name());
194                assert_eq!(sig1.start_bit(), sig2.start_bit());
195                assert_eq!(sig1.length(), sig2.length());
196                assert_eq!(sig1.byte_order(), sig2.byte_order());
197                assert_eq!(sig1.is_unsigned(), sig2.is_unsigned());
198                assert_eq!(sig1.factor(), sig2.factor());
199                assert_eq!(sig1.offset(), sig2.offset());
200                assert_eq!(sig1.min(), sig2.min());
201                assert_eq!(sig1.max(), sig2.max());
202                assert_eq!(sig1.unit(), sig2.unit());
203                assert_eq!(sig1.receivers(), sig2.receivers());
204            }
205        }
206    }
207
208    #[test]
209    fn test_save_basic() {
210        // Use parsing instead of builders
211        // Note: '*' is parsed as None per spec compliance, output is 'Vector__XXX'
212        let dbc_content = r#"VERSION "1.0"
213
214BU_: ECM
215
216BO_ 256 EngineData : 8 ECM
217 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" *
218"#;
219        let dbc = Dbc::parse(dbc_content).unwrap();
220
221        let saved = dbc.to_dbc_string();
222        assert!(saved.contains("VERSION \"1.0\""));
223        assert!(saved.contains("BU_: ECM"));
224        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
225        // Per DBC spec Section 9.5: '*' is not valid, we output 'Vector__XXX' instead
226        assert!(saved.contains("SG_ RPM : 0|16@0+ (0.25,0) [0|8000] \"rpm\" Vector__XXX"));
227    }
228
229    #[test]
230    fn test_save_multiple_messages() {
231        // Use parsing instead of builders
232        let dbc_content = r#"VERSION "1.0"
233
234BU_: ECM TCM
235
236BO_ 256 EngineData : 8 ECM
237 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
238
239BO_ 512 BrakeData : 4 TCM
240 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar"
241"#;
242        let dbc = Dbc::parse(dbc_content).unwrap();
243        let saved = dbc.to_dbc_string();
244
245        // Verify both messages are present
246        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
247        assert!(saved.contains("BO_ 512 BrakeData : 4 TCM"));
248        assert!(saved.contains("SG_ RPM"));
249        assert!(saved.contains("SG_ Pressure"));
250    }
251
252    #[test]
253    fn test_bit_timing_empty() {
254        // Empty BS_: should still be present in output
255        let dbc = Dbc::parse(
256            r#"VERSION "1.0"
257
258BS_:
259
260BU_: ECM
261
262BO_ 256 Engine : 8 ECM
263"#,
264        )
265        .unwrap();
266
267        // bit_timing should be None when empty
268        assert!(dbc.bit_timing().is_none());
269
270        // Output should still have BS_:
271        let saved = dbc.to_dbc_string();
272        assert!(saved.contains("BS_:"));
273    }
274
275    #[test]
276    fn test_bit_timing_with_values() {
277        let dbc = Dbc::parse(
278            r#"VERSION "1.0"
279
280BS_: 500000 : 1,2
281
282BU_: ECM
283
284BO_ 256 Engine : 8 ECM
285"#,
286        )
287        .unwrap();
288
289        // bit_timing should be Some with values
290        let bt = dbc.bit_timing().expect("bit timing should be present");
291        assert_eq!(bt.baudrate(), Some(500000));
292        assert_eq!(bt.btr1(), Some(1));
293        assert_eq!(bt.btr2(), Some(2));
294
295        // Output should have full BS_ line
296        let saved = dbc.to_dbc_string();
297        assert!(saved.contains("BS_: 500000 : 1,2"));
298    }
299
300    #[test]
301    fn test_bit_timing_baudrate_only() {
302        let dbc = Dbc::parse(
303            r#"VERSION "1.0"
304
305BS_: 500000
306
307BU_: ECM
308
309BO_ 256 Engine : 8 ECM
310"#,
311        )
312        .unwrap();
313
314        // bit_timing should be Some with baudrate only
315        let bt = dbc.bit_timing().expect("bit timing should be present");
316        assert_eq!(bt.baudrate(), Some(500000));
317        assert_eq!(bt.btr1(), None);
318        assert_eq!(bt.btr2(), None);
319
320        // Output should have BS_ with baudrate
321        let saved = dbc.to_dbc_string();
322        assert!(saved.contains("BS_: 500000"));
323    }
324}