dbc_rs/
version.rs

1use alloc::{
2    format,
3    string::{String, ToString},
4    vec::Vec,
5};
6
7use crate::{Error, error::messages};
8
9/// Represents the version string from a DBC file.
10///
11/// DBC files can specify a version in the format "major.minor.patch",
12/// where minor and patch are optional. This struct stores the parsed
13/// version components.
14///
15/// # Examples
16///
17/// ```rust
18/// use dbc_rs::Version;
19///
20/// let version = Version::builder()
21///     .major(1)
22///     .minor(0)
23///     .build()?;
24/// # Ok::<(), dbc_rs::Error>(())
25/// ```
26#[derive(Debug)]
27pub struct Version {
28    major: u8,
29    minor: Option<u8>,
30    patch: Option<u8>,
31}
32
33impl Version {
34    /// Create a new builder for constructing a `Version`
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use dbc_rs::Version;
40    ///
41    /// let v1 = Version::builder().major(1).build()?;
42    /// let v2 = Version::builder().major(1).minor(0).build()?;
43    /// let v3 = Version::builder().major(1).minor(2).patch(3).build()?;
44    /// # Ok::<(), dbc_rs::Error>(())
45    /// ```
46    pub fn builder() -> VersionBuilder {
47        VersionBuilder::new()
48    }
49
50    /// This is an internal constructor. For public API usage, use [`Version::builder()`] instead.
51    pub(crate) fn new(major: u8, minor: Option<u8>, patch: Option<u8>) -> Result<Self, Error> {
52        if minor.is_none() && patch.is_some() {
53            return Err(Error::Version(
54                messages::VERSION_PATCH_REQUIRES_MINOR.to_string(),
55            ));
56        }
57        Ok(Self {
58            major,
59            minor,
60            patch,
61        })
62    }
63
64    pub(super) fn parse(version: &str) -> Result<Self, Error> {
65        // Remove "VERSION " prefix
66        let version = if let Some(v) = version.strip_prefix("VERSION") {
67            v
68        } else {
69            return Err(Error::Version(messages::VERSION_EMPTY.to_string()));
70        }
71        .trim();
72
73        if version.is_empty() {
74            return Err(Error::Version(messages::VERSION_EMPTY.to_string()));
75        }
76
77        // Must be enclosed in double quotes
78        if !version.starts_with('"') || !version.ends_with('"') {
79            return Err(Error::Version(messages::VERSION_INVALID.to_string()));
80        }
81
82        let parts: Vec<&str> = version[1..version.len() - 1].split('.').collect();
83
84        // Min 1 and maximum 3 parts
85        if parts.is_empty() || parts.len() > 3 {
86            return Err(Error::Version(messages::VERSION_INVALID.to_string()));
87        }
88
89        // Parse parts
90        let major =
91            parts[0].parse().map_err(|e| Error::Version(messages::parse_number_failed(e)))?;
92        let minor: Option<u8> = if parts.len() > 1 {
93            Some(parts[1].parse().map_err(|e| Error::Version(messages::parse_number_failed(e)))?)
94        } else {
95            None
96        };
97        let patch: Option<u8> = if parts.len() > 2 {
98            Some(parts[2].parse().map_err(|e| Error::Version(messages::parse_number_failed(e)))?)
99        } else {
100            None
101        };
102
103        Ok(Version {
104            major,
105            minor,
106            patch,
107        })
108    }
109
110    /// Get the major version number
111    #[inline]
112    pub fn major(&self) -> u8 {
113        self.major
114    }
115
116    /// Get the minor version number, if present
117    #[inline]
118    pub fn minor(&self) -> Option<u8> {
119        self.minor
120    }
121
122    /// Get the patch version number, if present
123    #[inline]
124    pub fn patch(&self) -> Option<u8> {
125        self.patch
126    }
127
128    /// Format version as a string (e.g., "1.2.3" or "1.0")
129    #[allow(clippy::inherent_to_string)]
130    pub fn to_string(&self) -> String {
131        match (self.minor, self.patch) {
132            (Some(minor), Some(patch)) => format!("{}.{}.{}", self.major, minor, patch),
133            (Some(minor), None) => format!("{}.{}", self.major, minor),
134            (None, _) => format!("{}", self.major),
135        }
136    }
137
138    /// Format version in DBC file format (e.g., `VERSION "1.0"`)
139    ///
140    /// Useful for debugging and visualization of the version in DBC format.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use dbc_rs::Version;
146    ///
147    /// let version = Version::builder().major(1).minor(0).build()?;
148    /// assert_eq!(version.to_dbc_string(), "VERSION \"1.0\"");
149    /// # Ok::<(), dbc_rs::Error>(())
150    /// ```
151    pub fn to_dbc_string(&self) -> String {
152        format!("VERSION \"{}\"", self.to_string())
153    }
154}
155
156/// Builder for constructing a `Version` with a fluent API
157///
158/// This builder provides a more ergonomic way to construct `Version` instances.
159///
160/// # Examples
161///
162/// ```
163/// use dbc_rs::Version;
164///
165/// // Major version only
166/// let v1 = Version::builder().major(1).build()?;
167///
168/// // Major and minor
169/// let v2 = Version::builder().major(1).minor(0).build()?;
170///
171/// // Full version (major.minor.patch)
172/// let v3 = Version::builder().major(1).minor(2).patch(3).build()?;
173/// # Ok::<(), dbc_rs::Error>(())
174/// ```
175#[derive(Debug)]
176pub struct VersionBuilder {
177    major: Option<u8>,
178    minor: Option<u8>,
179    patch: Option<u8>,
180}
181
182impl VersionBuilder {
183    fn new() -> Self {
184        Self {
185            major: None,
186            minor: None,
187            patch: None,
188        }
189    }
190
191    /// Set the major version number (required)
192    pub fn major(mut self, major: u8) -> Self {
193        self.major = Some(major);
194        self
195    }
196
197    /// Set the minor version number (optional)
198    pub fn minor(mut self, minor: u8) -> Self {
199        self.minor = Some(minor);
200        self
201    }
202
203    /// Set the patch version number (optional, requires minor to be set)
204    pub fn patch(mut self, patch: u8) -> Self {
205        self.patch = Some(patch);
206        self
207    }
208
209    /// Validate the current builder state
210    ///
211    /// This method performs the same validation as `Version::validate()` but on the
212    /// builder's current state. Useful for checking validity before calling `build()`.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if:
217    /// - Required field (`major`) is missing
218    /// - `patch` is set but `minor` is not (patch requires minor)
219    pub fn validate(&self) -> Result<(), Error> {
220        let _major = self
221            .major
222            .ok_or_else(|| Error::Version(messages::VERSION_MAJOR_REQUIRED.to_string()))?;
223
224        if self.minor.is_none() && self.patch.is_some() {
225            return Err(Error::Version(
226                messages::VERSION_PATCH_REQUIRES_MINOR.to_string(),
227            ));
228        }
229
230        Ok(())
231    }
232
233    /// Build the `Version` from the builder
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if:
238    /// - Required field (`major`) is missing
239    /// - `patch` is set but `minor` is not (patch requires minor)
240    pub fn build(self) -> Result<Version, Error> {
241        let major = self
242            .major
243            .ok_or_else(|| Error::Version(messages::VERSION_MAJOR_REQUIRED.to_string()))?;
244
245        Version::new(major, self.minor, self.patch)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::Version;
252    use crate::error::lang;
253    use crate::{Error, error::messages};
254
255    #[test]
256    fn test_read_version() {
257        let line = "VERSION \"1.0\"";
258        let version = Version::parse(line).unwrap();
259        assert_eq!(version.major(), 1);
260        assert_eq!(version.minor(), Some(0));
261        assert_eq!(version.patch(), None);
262    }
263
264    #[test]
265    fn test_read_version_invalid() {
266        let line = "VERSION 1.0";
267        let version = Version::parse(line).unwrap_err();
268        assert_eq!(
269            version,
270            Error::Version(messages::VERSION_INVALID.to_string())
271        );
272    }
273
274    #[test]
275    fn test_version_new() {
276        let v1 = Version::new(1, None, None).unwrap();
277        assert_eq!(v1.major(), 1);
278        assert_eq!(v1.minor(), None);
279        assert_eq!(v1.patch(), None);
280        assert_eq!(v1.to_string(), "1");
281
282        let v2 = Version::new(2, Some(3), None).unwrap();
283        assert_eq!(v2.major(), 2);
284        assert_eq!(v2.minor(), Some(3));
285        assert_eq!(v2.patch(), None);
286        assert_eq!(v2.to_string(), "2.3");
287
288        let v3 = Version::new(4, Some(5), Some(6)).unwrap();
289        assert_eq!(v3.major(), 4);
290        assert_eq!(v3.minor(), Some(5));
291        assert_eq!(v3.patch(), Some(6));
292        assert_eq!(v3.to_string(), "4.5.6");
293    }
294
295    #[test]
296    fn test_version_new_invalid_patch_without_minor() {
297        let result = Version::new(1, None, Some(2));
298        assert!(result.is_err());
299        assert_eq!(
300            result.unwrap_err(),
301            Error::Version(messages::VERSION_PATCH_REQUIRES_MINOR.to_string())
302        );
303    }
304
305    #[test]
306    fn test_version_parse_empty() {
307        let result = Version::parse("");
308        assert!(result.is_err());
309        match result.unwrap_err() {
310            Error::Version(msg) => assert!(msg.contains(lang::VERSION_EMPTY)),
311            _ => panic!("Expected InvalidData error"),
312        }
313    }
314
315    #[test]
316    fn test_version_parse_no_version_prefix() {
317        let result = Version::parse("\"1.0\"");
318        assert!(result.is_err());
319        match result.unwrap_err() {
320            Error::Version(msg) => assert!(msg.contains(lang::VERSION_EMPTY)),
321            _ => panic!("Expected InvalidData error"),
322        }
323    }
324
325    #[test]
326    fn test_version_parse_no_quotes() {
327        let result = Version::parse("VERSION 1.0");
328        assert!(result.is_err());
329        match result.unwrap_err() {
330            Error::Version(msg) => assert!(msg.contains(lang::VERSION_INVALID)),
331            _ => panic!("Expected InvalidData error"),
332        }
333    }
334
335    #[test]
336    fn test_version_parse_too_many_parts() {
337        let result = Version::parse("VERSION \"1.2.3.4\"");
338        assert!(result.is_err());
339        match result.unwrap_err() {
340            Error::Version(msg) => assert!(msg.contains(lang::VERSION_INVALID)),
341            _ => panic!("Expected InvalidData error"),
342        }
343    }
344
345    #[test]
346    fn test_version_parse_invalid_number() {
347        let result = Version::parse("VERSION \"abc\"");
348        assert!(result.is_err());
349        // This should trigger ParseIntError conversion
350        match result.unwrap_err() {
351            Error::Version(msg) => {
352                // Check for format template text (language-agnostic)
353                // Check for format template text (language-agnostic) - extract text before first placeholder
354                let template_text = lang::FORMAT_PARSE_NUMBER_FAILED.split("{}").next().unwrap();
355                assert!(msg.contains(template_text.trim_end_matches(':').trim_end()));
356            }
357            _ => panic!("Expected Version error from ParseIntError"),
358        }
359    }
360
361    #[test]
362    fn test_version_to_string_all_variants() {
363        let v1 = Version::new(1, None, None).unwrap();
364        assert_eq!(v1.to_string(), "1");
365
366        let v2 = Version::new(2, Some(3), None).unwrap();
367        assert_eq!(v2.to_string(), "2.3");
368
369        let v3 = Version::new(4, Some(5), Some(6)).unwrap();
370        assert_eq!(v3.to_string(), "4.5.6");
371    }
372
373    #[test]
374    fn test_version_to_dbc_string() {
375        let v1 = Version::new(1, None, None).unwrap();
376        assert_eq!(v1.to_dbc_string(), "VERSION \"1\"");
377
378        let v2 = Version::new(1, Some(0), None).unwrap();
379        assert_eq!(v2.to_dbc_string(), "VERSION \"1.0\"");
380
381        let v3 = Version::new(2, Some(3), Some(4)).unwrap();
382        assert_eq!(v3.to_dbc_string(), "VERSION \"2.3.4\"");
383    }
384}