dbc_rs/dbc/
std.rs

1use super::Dbc;
2use std::fmt::{Display, Formatter, Result};
3
4impl Dbc {
5    /// Serialize this DBC to a DBC format string
6    ///
7    /// # Examples
8    ///
9    /// ```rust,no_run
10    /// use dbc_rs::Dbc;
11    ///
12    /// let dbc = Dbc::parse("VERSION \"1.0\"\n\nBU_: ECM\n\nBO_ 256 Engine : 8 ECM")?;
13    /// let dbc_string = dbc.to_dbc_string();
14    /// // The string can be written to a file or used elsewhere
15    /// assert!(dbc_string.contains("VERSION"));
16    /// # Ok::<(), dbc_rs::Error>(())
17    /// ```
18    #[must_use = "return value should be used"]
19    pub fn to_dbc_string(&self) -> String {
20        // Pre-allocate with estimated capacity
21        // Estimate: ~50 chars per message + ~100 chars per signal
22        let signal_count: usize = self.messages().iter().map(|m| m.signals().len()).sum();
23        let estimated_capacity = 200 + (self.messages.len() * 50) + (signal_count * 100);
24        let mut result = String::with_capacity(estimated_capacity);
25
26        // VERSION line
27        if let Some(version) = &self.version {
28            result.push_str(&version.to_dbc_string());
29            result.push_str("\n\n");
30        }
31
32        // BU_ line
33        result.push_str(&self.nodes.to_dbc_string());
34        result.push('\n');
35
36        // BO_ and SG_ lines for each message
37        for message in self.messages().iter() {
38            result.push('\n');
39            result.push_str(&message.to_string_full());
40        }
41
42        // CM_ lines (comments section)
43        // General database comment
44        if let Some(comment) = self.comment() {
45            result.push_str("\nCM_ \"");
46            result.push_str(comment);
47            result.push_str("\";\n");
48        }
49
50        // Node comments
51        for node in self.nodes.iter_nodes() {
52            if let Some(comment) = node.comment() {
53                result.push_str("CM_ BU_ ");
54                result.push_str(node.name());
55                result.push_str(" \"");
56                result.push_str(comment);
57                result.push_str("\";\n");
58            }
59        }
60
61        // Message and signal comments
62        for message in self.messages().iter() {
63            if let Some(comment) = message.comment() {
64                result.push_str("CM_ BO_ ");
65                result.push_str(&message.id().to_string());
66                result.push_str(" \"");
67                result.push_str(comment);
68                result.push_str("\";\n");
69            }
70
71            for signal in message.signals().iter() {
72                if let Some(comment) = signal.comment() {
73                    result.push_str("CM_ SG_ ");
74                    result.push_str(&message.id().to_string());
75                    result.push(' ');
76                    result.push_str(signal.name());
77                    result.push_str(" \"");
78                    result.push_str(comment);
79                    result.push_str("\";\n");
80                }
81            }
82        }
83
84        result
85    }
86}
87
88impl Display for Dbc {
89    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
90        write!(f, "{}", self.to_dbc_string())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use crate::Dbc;
97
98    #[test]
99    fn test_to_dbc_string() {
100        let dbc = Dbc::parse(
101            r#"VERSION "1.0"
102
103BU_: ECM
104
105BO_ 256 Engine : 8 ECM
106 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm"
107"#,
108        )
109        .unwrap();
110
111        let dbc_string = dbc.to_dbc_string();
112        assert!(dbc_string.contains("VERSION"));
113        assert!(dbc_string.contains("BU_"));
114        assert!(dbc_string.contains("BO_"));
115        assert!(dbc_string.contains("SG_"));
116    }
117
118    #[test]
119    fn test_display() {
120        let dbc = Dbc::parse(
121            r#"VERSION "1.0"
122
123BU_: ECM
124
125BO_ 256 Engine : 8 ECM
126"#,
127        )
128        .unwrap();
129
130        let display_str = format!("{}", dbc);
131        assert!(display_str.contains("VERSION"));
132    }
133
134    #[test]
135    fn test_save_round_trip() {
136        let original = r#"VERSION "1.0"
137
138BU_: ECM TCM
139
140BO_ 256 EngineData : 8 ECM
141 SG_ RPM : 0|16@1+ (0.25,0) [0|8000] "rpm" *
142 SG_ Temperature : 16|8@0- (1,-40) [-40|215] "°C" TCM
143
144BO_ 512 BrakeData : 4 TCM
145 SG_ Pressure : 0|16@0+ (0.1,0) [0|1000] "bar"
146"#;
147
148        let dbc = Dbc::parse(original).unwrap();
149        let saved = dbc.to_dbc_string();
150        let dbc2 = Dbc::parse(&saved).unwrap();
151
152        // Verify round-trip: parsed data should match
153        assert_eq!(
154            dbc.version().map(|v| v.to_string()),
155            dbc2.version().map(|v| v.to_string())
156        );
157        assert_eq!(dbc.messages().len(), dbc2.messages().len());
158
159        for (msg1, msg2) in dbc.messages().iter().zip(dbc2.messages().iter()) {
160            assert_eq!(msg1.id(), msg2.id());
161            assert_eq!(msg1.name(), msg2.name());
162            assert_eq!(msg1.dlc(), msg2.dlc());
163            assert_eq!(msg1.sender(), msg2.sender());
164            assert_eq!(msg1.signals().len(), msg2.signals().len());
165
166            for (sig1, sig2) in msg1.signals().iter().zip(msg2.signals().iter()) {
167                assert_eq!(sig1.name(), sig2.name());
168                assert_eq!(sig1.start_bit(), sig2.start_bit());
169                assert_eq!(sig1.length(), sig2.length());
170                assert_eq!(sig1.byte_order(), sig2.byte_order());
171                assert_eq!(sig1.is_unsigned(), sig2.is_unsigned());
172                assert_eq!(sig1.factor(), sig2.factor());
173                assert_eq!(sig1.offset(), sig2.offset());
174                assert_eq!(sig1.min(), sig2.min());
175                assert_eq!(sig1.max(), sig2.max());
176                assert_eq!(sig1.unit(), sig2.unit());
177                assert_eq!(sig1.receivers(), sig2.receivers());
178            }
179        }
180    }
181
182    #[test]
183    fn test_save_basic() {
184        // Use parsing instead of builders
185        // Note: '*' is parsed as None per spec compliance, output is 'Vector__XXX'
186        let dbc_content = r#"VERSION "1.0"
187
188BU_: ECM
189
190BO_ 256 EngineData : 8 ECM
191 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm" *
192"#;
193        let dbc = Dbc::parse(dbc_content).unwrap();
194
195        let saved = dbc.to_dbc_string();
196        assert!(saved.contains("VERSION \"1.0\""));
197        assert!(saved.contains("BU_: ECM"));
198        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
199        // Per DBC spec Section 9.5: '*' is not valid, we output 'Vector__XXX' instead
200        assert!(saved.contains("SG_ RPM : 0|16@0+ (0.25,0) [0|8000] \"rpm\" Vector__XXX"));
201    }
202
203    #[test]
204    fn test_save_multiple_messages() {
205        // Use parsing instead of builders
206        let dbc_content = r#"VERSION "1.0"
207
208BU_: ECM TCM
209
210BO_ 256 EngineData : 8 ECM
211 SG_ RPM : 0|16@0+ (0.25,0) [0|8000] "rpm"
212
213BO_ 512 BrakeData : 4 TCM
214 SG_ Pressure : 0|16@1+ (0.1,0) [0|1000] "bar"
215"#;
216        let dbc = Dbc::parse(dbc_content).unwrap();
217        let saved = dbc.to_dbc_string();
218
219        // Verify both messages are present
220        assert!(saved.contains("BO_ 256 EngineData : 8 ECM"));
221        assert!(saved.contains("BO_ 512 BrakeData : 4 TCM"));
222        assert!(saved.contains("SG_ RPM"));
223        assert!(saved.contains("SG_ Pressure"));
224    }
225}