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        // BU_ line
50        result.push_str(&self.nodes.to_dbc_string());
51        result.push('\n');
52
53        // BO_ and SG_ lines for each message
54        for message in self.messages().iter() {
55            result.push('\n');
56            result.push_str(&message.to_string_full());
57        }
58
59        // CM_ lines (comments section)
60        // General database comment
61        if let Some(comment) = self.comment() {
62            result.push_str("\nCM_ \"");
63            result.push_str(comment);
64            result.push_str("\";\n");
65        }
66
67        // Node comments
68        for node in self.nodes.iter_nodes() {
69            if let Some(comment) = node.comment() {
70                result.push_str("CM_ BU_ ");
71                result.push_str(node.name());
72                result.push_str(" \"");
73                result.push_str(comment);
74                result.push_str("\";\n");
75            }
76        }
77
78        // Message and signal comments
79        for message in self.messages().iter() {
80            if let Some(comment) = message.comment() {
81                result.push_str("CM_ BO_ ");
82                result.push_str(&message.id().to_string());
83                result.push_str(" \"");
84                result.push_str(comment);
85                result.push_str("\";\n");
86            }
87
88            for signal in message.signals().iter() {
89                if let Some(comment) = signal.comment() {
90                    result.push_str("CM_ SG_ ");
91                    result.push_str(&message.id().to_string());
92                    result.push(' ');
93                    result.push_str(signal.name());
94                    result.push_str(" \"");
95                    result.push_str(comment);
96                    result.push_str("\";\n");
97                }
98            }
99        }
100
101        result
102    }
103}
104
105impl Display for Dbc {
106    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
107        write!(f, "{}", self.to_dbc_string())
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use crate::Dbc;
114
115    #[test]
116    fn test_to_dbc_string() {
117        let dbc = Dbc::parse(
118            r#"VERSION "1.0"
119
120BU_: ECM
121
122BO_ 256 Engine : 8 ECM
123 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
124"#,
125        )
126        .unwrap();
127
128        let dbc_string = dbc.to_dbc_string();
129        assert!(dbc_string.contains("VERSION"));
130        assert!(dbc_string.contains("BU_"));
131        assert!(dbc_string.contains("BO_"));
132        assert!(dbc_string.contains("SG_"));
133    }
134
135    #[test]
136    fn test_display() {
137        let dbc = Dbc::parse(
138            r#"VERSION "1.0"
139
140BU_: ECM
141
142BO_ 256 Engine : 8 ECM
143"#,
144        )
145        .unwrap();
146
147        let display_str = format!("{}", dbc);
148        assert!(display_str.contains("VERSION"));
149    }
150
151    #[test]
152    fn test_save_round_trip() {
153        let original = r#"VERSION "1.0"
154
155BU_: ECM TCM
156
157BO_ 256 EngineData : 8 ECM
158 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
159 SG_ Temperature : 16|8@0- (1,-40) [-40|215] "°C" TCM
160
161BO_ 512 BrakeData : 4 TCM
162 SG_ Pressure : 0|16@0+ (0.1,0) [0|1000] "bar"
163"#;
164
165        let dbc = Dbc::parse(original).unwrap();
166        let saved = dbc.to_dbc_string();
167        let dbc2 = Dbc::parse(&saved).unwrap();
168
169        // Verify round-trip: parsed data should match
170        assert_eq!(
171            dbc.version().map(|v| v.to_string()),
172            dbc2.version().map(|v| v.to_string())
173        );
174        assert_eq!(dbc.messages().len(), dbc2.messages().len());
175
176        for (msg1, msg2) in dbc.messages().iter().zip(dbc2.messages().iter()) {
177            assert_eq!(msg1.id(), msg2.id());
178            assert_eq!(msg1.name(), msg2.name());
179            assert_eq!(msg1.dlc(), msg2.dlc());
180            assert_eq!(msg1.sender(), msg2.sender());
181            assert_eq!(msg1.signals().len(), msg2.signals().len());
182
183            for (sig1, sig2) in msg1.signals().iter().zip(msg2.signals().iter()) {
184                assert_eq!(sig1.name(), sig2.name());
185                assert_eq!(sig1.start_bit(), sig2.start_bit());
186                assert_eq!(sig1.length(), sig2.length());
187                assert_eq!(sig1.byte_order(), sig2.byte_order());
188                assert_eq!(sig1.is_unsigned(), sig2.is_unsigned());
189                assert_eq!(sig1.factor(), sig2.factor());
190                assert_eq!(sig1.offset(), sig2.offset());
191                assert_eq!(sig1.min(), sig2.min());
192                assert_eq!(sig1.max(), sig2.max());
193                assert_eq!(sig1.unit(), sig2.unit());
194                assert_eq!(sig1.receivers(), sig2.receivers());
195            }
196        }
197    }
198
199    #[test]
200    fn test_save_basic() {
201        // Use parsing instead of builders
202        // Note: '*' is parsed as None per spec compliance, output is 'Vector__XXX'
203        let dbc_content = r#"VERSION "1.0"
204
205BU_: ECM
206
207BO_ 256 EngineData : 8 ECM
208 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" *
209"#;
210        let dbc = Dbc::parse(dbc_content).unwrap();
211
212        let saved = dbc.to_dbc_string();
213        assert!(saved.contains("VERSION \"1.0\""));
214        assert!(saved.contains("BU_: ECM"));
215        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
216        // Per DBC spec Section 9.5: '*' is not valid, we output 'Vector__XXX' instead
217        assert!(saved.contains("SG_ RPM : 0|16@0+ (0.25,0) [0|8000] \"rpm\" Vector__XXX"));
218    }
219
220    #[test]
221    fn test_save_multiple_messages() {
222        // Use parsing instead of builders
223        let dbc_content = r#"VERSION "1.0"
224
225BU_: ECM TCM
226
227BO_ 256 EngineData : 8 ECM
228 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
229
230BO_ 512 BrakeData : 4 TCM
231 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar"
232"#;
233        let dbc = Dbc::parse(dbc_content).unwrap();
234        let saved = dbc.to_dbc_string();
235
236        // Verify both messages are present
237        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
238        assert!(saved.contains("BO_ 512 BrakeData : 4 TCM"));
239        assert!(saved.contains("SG_ RPM"));
240        assert!(saved.contains("SG_ Pressure"));
241    }
242}