dbc_rs/
nodes.rs

1use crate::{Error, error::messages};
2use alloc::{boxed::Box, string::String, vec::Vec};
3
4/// Represents the list of nodes (ECUs) defined in a DBC file.
5///
6/// Nodes represent the electronic control units (ECUs) that participate
7/// in the CAN bus communication. Each message must have a sender that
8/// is present in the nodes list.
9///
10/// # Examples
11///
12/// ```rust
13/// use dbc_rs::Nodes;
14///
15/// let nodes = Nodes::builder()
16///     .add_node("ECM")
17///     .add_node("TCM")
18///     .build()?;
19/// # Ok::<(), dbc_rs::Error>(())
20/// ```
21#[derive(Debug)]
22pub struct Nodes {
23    nodes: Vec<Box<str>>,
24}
25
26impl Nodes {
27    /// Create a new builder for constructing a `Nodes`
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use dbc_rs::Nodes;
33    ///
34    /// let nodes = Nodes::builder()
35    ///     .add_node("ECM")
36    ///     .add_node("TCM")
37    ///     .add_node("BCM")
38    ///     .build()
39    ///     .unwrap();
40    /// assert!(nodes.contains("ECM"));
41    /// assert!(nodes.contains("TCM"));
42    /// ```
43    pub fn builder() -> NodesBuilder {
44        NodesBuilder::new()
45    }
46
47    /// This is an internal constructor. For public API usage, use [`Nodes::builder()`] instead.
48    #[allow(dead_code)] // Used in tests
49    pub(crate) fn new<I, S>(nodes: I) -> Result<Self, Error>
50    where
51        I: IntoIterator<Item = S>,
52        S: AsRef<str>,
53    {
54        let nodes: Vec<Box<str>> = nodes.into_iter().map(|s| s.as_ref().into()).collect();
55        Self::validate(&nodes)?;
56        Ok(Self { nodes })
57    }
58
59    /// Validate node names according to DBC format specifications
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if:
64    /// - Duplicate node names are found (case-sensitive)
65    fn validate(nodes: &[Box<str>]) -> Result<(), Error> {
66        // Check for duplicate node names (case-sensitive)
67        for (i, node1) in nodes.iter().enumerate() {
68            for node2 in nodes.iter().skip(i + 1) {
69                if node1.as_ref() == node2.as_ref() {
70                    return Err(Error::Nodes(messages::duplicate_node_name(node1.as_ref())));
71                }
72            }
73        }
74        Ok(())
75    }
76
77    pub(super) fn parse(nodes: &str) -> Result<Self, Error> {
78        let nodes: Vec<Box<str>> = nodes[4..].split_whitespace().map(|s| s.into()).collect();
79        Self::validate(&nodes)?;
80        Ok(Self { nodes })
81    }
82
83    /// Get a read-only slice of node names.
84    ///
85    /// Returns `None` if there are no nodes, otherwise returns `Some(&[Box<str>])`.
86    #[inline]
87    pub fn nodes(&self) -> Option<&[Box<str>]> {
88        if self.nodes.is_empty() {
89            None
90        } else {
91            Some(self.nodes.as_ref())
92        }
93    }
94
95    /// Check if a node name exists in the list
96    #[inline]
97    pub fn contains(&self, node: &str) -> bool {
98        self.nodes.iter().any(|n| n.as_ref() == node)
99    }
100
101    /// Format nodes as a space-separated string for saving
102    #[allow(clippy::inherent_to_string)]
103    pub fn to_string(&self) -> String {
104        if self.nodes.is_empty() {
105            return String::new();
106        }
107        // Pre-allocate: estimate ~10 chars per node name + spaces
108        let capacity = self.nodes.len() * 10;
109        let mut result = String::with_capacity(capacity);
110        for (i, node) in self.nodes.iter().enumerate() {
111            if i > 0 {
112                result.push(' ');
113            }
114            result.push_str(node.as_ref());
115        }
116        result
117    }
118
119    /// Check if the nodes list is empty
120    #[inline]
121    pub fn is_empty(&self) -> bool {
122        self.nodes.is_empty()
123    }
124
125    /// Format nodes in DBC file format (e.g., `BU_: ECM TCM`)
126    ///
127    /// Useful for debugging and visualization of the nodes in DBC format.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use dbc_rs::Nodes;
133    ///
134    /// let nodes = Nodes::builder()
135    ///     .add_node("ECM")
136    ///     .add_node("TCM")
137    ///     .build()
138    ///     .unwrap();
139    /// assert_eq!(nodes.to_dbc_string(), "BU_: ECM TCM");
140    /// ```
141    pub fn to_dbc_string(&self) -> String {
142        let mut result = String::from("BU_:");
143        let nodes_str = self.to_string();
144        if !nodes_str.is_empty() {
145            result.push(' ');
146            result.push_str(&nodes_str);
147        }
148        result
149    }
150}
151
152/// Builder for constructing a `Nodes` with a fluent API
153///
154/// This builder provides a more ergonomic way to construct `Nodes` instances.
155///
156/// # Examples
157///
158/// ```
159/// use dbc_rs::Nodes;
160///
161/// // Add nodes one by one
162/// let nodes = Nodes::builder()
163///     .add_node("ECM")
164///     .add_node("TCM")
165///     .add_node("BCM")
166///     .build();
167///
168/// // Add nodes from an iterator
169/// let nodes = Nodes::builder()
170///     .add_nodes(&["ECM", "TCM", "BCM"])
171///     .build();
172/// ```
173#[derive(Debug)]
174pub struct NodesBuilder {
175    nodes: Vec<Box<str>>,
176}
177
178impl NodesBuilder {
179    fn new() -> Self {
180        Self { nodes: Vec::new() }
181    }
182
183    /// Add a single node
184    pub fn add_node(mut self, node: impl AsRef<str>) -> Self {
185        self.nodes.push(node.as_ref().into());
186        self
187    }
188
189    /// Add multiple nodes from an iterator
190    pub fn add_nodes<I, S>(mut self, nodes: I) -> Self
191    where
192        I: IntoIterator<Item = S>,
193        S: AsRef<str>,
194    {
195        self.nodes.extend(nodes.into_iter().map(|s| s.as_ref().into()));
196        self
197    }
198
199    /// Clear all nodes
200    pub fn clear(mut self) -> Self {
201        self.nodes.clear();
202        self
203    }
204
205    /// Validate the current builder state
206    ///
207    /// This method validates the builder's current state according to DBC format
208    /// specifications. Currently checks for duplicate node names.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if:
213    /// - Duplicate node names are found (case-sensitive)
214    pub fn validate(&self) -> Result<(), Error> {
215        Nodes::validate(&self.nodes)
216    }
217
218    /// Build the `Nodes` from the builder
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if:
223    /// - Duplicate node names are found (case-sensitive)
224    pub fn build(self) -> Result<Nodes, Error> {
225        Nodes::validate(&self.nodes)?;
226        Ok(Nodes { nodes: self.nodes })
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::error::lang;
234
235    #[test]
236    fn test_nodes_from_valid_line() {
237        let line = "BU_: ECM TCM BCM ABS";
238        let nodes = Nodes::parse(line).unwrap();
239        let node_slice = nodes.nodes().unwrap();
240        assert_eq!(
241            node_slice,
242            &["ECM".into(), "TCM".into(), "BCM".into(), "ABS".into()]
243        );
244    }
245
246    #[test]
247    fn test_nodes_from_single_node() {
248        let line = "BU_: ONLYONE";
249        let nodes = Nodes::parse(line).unwrap();
250        let node_slice = nodes.nodes().unwrap();
251        assert_eq!(node_slice, &["ONLYONE".into()]);
252    }
253
254    #[test]
255    fn test_nodes_from_with_extra_spaces() {
256        let line = "BU_:   Node1   Node2   ";
257        let nodes = Nodes::parse(line).unwrap();
258        let node_slice = nodes.nodes().unwrap();
259        assert_eq!(node_slice, &["Node1".into(), "Node2".into()]);
260    }
261
262    #[test]
263    fn test_nodes_from_empty_list() {
264        let line = "BU_:";
265        let nodes = Nodes::parse(line).unwrap();
266        assert!(nodes.nodes().is_none());
267    }
268
269    #[test]
270    fn test_nodes_new() {
271        let nodes = Nodes::new(["ECM", "TCM", "BCM"]).unwrap();
272        assert!(nodes.contains("ECM"));
273        assert!(nodes.contains("TCM"));
274        assert!(nodes.contains("BCM"));
275        assert!(!nodes.contains("ABS"));
276        assert_eq!(nodes.nodes().unwrap().len(), 3);
277    }
278
279    #[test]
280    fn test_nodes_new_from_vec() {
281        let node_vec = vec!["Node1", "Node2", "Node3"];
282        let nodes = Nodes::new(node_vec).unwrap();
283        assert!(nodes.contains("Node1"));
284        assert_eq!(nodes.nodes().unwrap().len(), 3);
285    }
286
287    #[test]
288    fn test_nodes_new_from_slice() {
289        let node_slice = &["A", "B", "C"][..];
290        let nodes = Nodes::new(node_slice).unwrap();
291        assert!(nodes.contains("A"));
292        assert_eq!(nodes.nodes().unwrap().len(), 3);
293    }
294
295    #[test]
296    fn test_nodes_new_duplicate() {
297        let result = Nodes::new(["ECM", "TCM", "ECM"]);
298        assert!(result.is_err());
299        match result.unwrap_err() {
300            Error::Nodes(msg) => assert!(msg.contains(lang::NODES_DUPLICATE_NAME)),
301            _ => panic!("Expected Nodes error"),
302        }
303    }
304
305    #[test]
306    fn test_nodes_to_string_single() {
307        let nodes = Nodes::new(["ECM"]).unwrap();
308        assert_eq!(nodes.to_string(), "ECM");
309    }
310
311    #[test]
312    fn test_nodes_to_string_multiple() {
313        let nodes = Nodes::new(["ECM", "TCM", "BCM"]).unwrap();
314        assert_eq!(nodes.to_string(), "ECM TCM BCM");
315    }
316
317    #[test]
318    fn test_nodes_to_dbc_string() {
319        let nodes_single = Nodes::new(["ECM"]).unwrap();
320        assert_eq!(nodes_single.to_dbc_string(), "BU_: ECM");
321
322        let nodes_multiple = Nodes::new(["ECM", "TCM", "BCM"]).unwrap();
323        assert_eq!(nodes_multiple.to_dbc_string(), "BU_: ECM TCM BCM");
324    }
325
326    #[test]
327    fn test_nodes_builder_duplicate() {
328        let result = Nodes::builder().add_node("ECM").add_node("TCM").add_node("ECM").build();
329        assert!(result.is_err());
330        match result.unwrap_err() {
331            Error::Nodes(msg) => assert!(msg.contains(lang::NODES_DUPLICATE_NAME)),
332            _ => panic!("Expected Nodes error"),
333        }
334    }
335
336    #[test]
337    fn test_nodes_parse_duplicate() {
338        let line = "BU_: ECM TCM ECM";
339        let result = Nodes::parse(line);
340        assert!(result.is_err());
341        match result.unwrap_err() {
342            Error::Nodes(msg) => assert!(msg.contains(lang::NODES_DUPLICATE_NAME)),
343            _ => panic!("Expected Nodes error"),
344        }
345    }
346}