Skip to main content

timelake_format/
version.rs

1//! The table format version, `timelake.format-version`: a semantic version of its own, separate
2//! from the software's.
3
4use std::fmt;
5use std::str::FromStr;
6
7use crate::{FormatError, Result};
8
9/// A table format version, `MAJOR.MINOR.PATCH`. What a bump means for an older Timelake:
10///
11/// | Bump | Reads the table | Writes the table |
12/// | --- | --- | --- |
13/// | patch (0.2.0 → 0.2.1, 1.2.0 → 1.2.1) | yes | yes |
14/// | minor from 1.0 (1.2 → 1.3) | yes | no |
15/// | major from 1.0, minor before (0.2 → 0.3, 1.x → 2.0) | no | no |
16///
17/// Before 1.0 the minor version is the breaking one, as Cargo treats `0.y` crates.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct FormatVersion {
20    /// A newer major (from 1.0) breaks older readers.
21    pub major: u32,
22    /// A newer minor breaks older writers (from 1.0), or older readers and writers (before 1.0).
23    pub minor: u32,
24    /// A newer patch is compatible both ways.
25    pub patch: u32,
26}
27
28impl FormatVersion {
29    /// Tables written by Timelake 0.1 and 0.2, which recorded no version.
30    pub const UNVERSIONED: Self = Self::new(0, 1, 0);
31
32    /// `MAJOR.MINOR.PATCH`.
33    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
34        Self { major, minor, patch }
35    }
36
37    /// Whether a Timelake that knows format `known` reads a table of this format correctly.
38    pub fn readable_by(self, known: Self) -> bool {
39        self.breaking() <= known.breaking()
40    }
41
42    /// Whether a Timelake that knows format `known` may write to a table of this format without
43    /// breaking what it relies on.
44    pub fn writable_by(self, known: Self) -> bool {
45        (self.major, self.minor) <= (known.major, known.minor)
46    }
47
48    /// The part of the version whose increase breaks older readers.
49    fn breaking(self) -> (u32, u32) {
50        if self.major == 0 { (0, self.minor) } else { (self.major, 0) }
51    }
52}
53
54impl fmt::Display for FormatVersion {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
57    }
58}
59
60impl FromStr for FormatVersion {
61    type Err = FormatError;
62
63    /// `MAJOR.MINOR.PATCH`, or the plain integer `N` that Timelake 0.3.0 wrote, read as `0.N.0`.
64    fn from_str(s: &str) -> Result<Self> {
65        let bad = || FormatError::new(format!("a format version is MAJOR.MINOR.PATCH, got {s:?}"));
66        let parts = s.trim().split('.').map(str::parse).collect::<std::result::Result<Vec<u32>, _>>();
67        match parts.map_err(|_| bad())?[..] {
68            [major, minor, patch] => Ok(Self::new(major, minor, patch)),
69            [legacy] => Ok(Self::new(0, legacy, 0)),
70            _ => Err(bad()),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn v(s: &str) -> FormatVersion {
80        s.parse().unwrap()
81    }
82
83    #[test]
84    fn parses_semver_and_the_legacy_integer() {
85        assert_eq!(v("0.2.0"), FormatVersion::new(0, 2, 0));
86        assert_eq!(v(" 1.12.3 ").to_string(), "1.12.3");
87        assert_eq!(v("2"), v("0.2.0"));
88        assert_eq!(v("1"), FormatVersion::UNVERSIONED);
89        for bad in ["", "1.2", "1.2.3.4", "v1.2.3", "1.-2.3"] {
90            assert!(bad.parse::<FormatVersion>().is_err(), "{bad:?}");
91        }
92    }
93
94    #[test]
95    fn compatibility_follows_the_bump() {
96        let known = v("0.2.0");
97        assert!(v("0.1.0").readable_by(known) && v("0.1.0").writable_by(known));
98        assert!(v("0.2.5").readable_by(known) && v("0.2.5").writable_by(known));
99        assert!(!v("0.3.0").readable_by(known) && !v("0.3.0").writable_by(known));
100        assert!(!v("1.0.0").readable_by(known));
101
102        let known = v("1.2.0");
103        assert!(v("0.9.0").readable_by(known) && v("0.9.0").writable_by(known));
104        assert!(v("1.2.7").writable_by(known));
105        assert!(v("1.3.0").readable_by(known) && !v("1.3.0").writable_by(known));
106        assert!(!v("2.0.0").readable_by(known) && !v("2.0.0").writable_by(known));
107    }
108}