Skip to main content

pardarsh_core/
schema.rs

1//! Schema versions and the compatibility policy.
2//!
3//! Versions are `MAJOR.MINOR`. A reader at `R` can read data written at `W`
4//! when `W.major == R.major && W.minor <= R.minor`: minor versions only add
5//! optional things, major versions may break. Data written by a *newer* minor
6//! version is rejected rather than silently dropping fields it may carry.
7
8use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct SchemaVersion {
15    pub major: u16,
16    pub minor: u16,
17}
18
19/// Version of the common record envelope and event format defined by this crate.
20pub const ENVELOPE_SCHEMA_VERSION: SchemaVersion = SchemaVersion::new(1, 0);
21
22/// Version of the export bundle format.
23pub const BUNDLE_FORMAT_VERSION: SchemaVersion = SchemaVersion::new(1, 0);
24
25impl SchemaVersion {
26    pub const fn new(major: u16, minor: u16) -> Self {
27        Self { major, minor }
28    }
29
30    /// Can a reader that understands `self` read data written at `written`?
31    pub fn can_read(&self, written: &SchemaVersion) -> bool {
32        self.major == written.major && written.minor <= self.minor
33    }
34}
35
36impl fmt::Display for SchemaVersion {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}.{}", self.major, self.minor)
39    }
40}
41
42impl FromStr for SchemaVersion {
43    type Err = String;
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        let (major, minor) = s.split_once('.').ok_or_else(|| format!("invalid schema version {s:?}"))?;
46        let parse = |p: &str| {
47            if p.is_empty() || (p.len() > 1 && p.starts_with('0')) {
48                return Err(format!("invalid schema version {s:?}"));
49            }
50            p.parse::<u16>().map_err(|_| format!("invalid schema version {s:?}"))
51        };
52        Ok(Self { major: parse(major)?, minor: parse(minor)? })
53    }
54}
55
56impl Serialize for SchemaVersion {
57    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
58        s.collect_str(self)
59    }
60}
61
62impl<'de> Deserialize<'de> for SchemaVersion {
63    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
64        let s = String::deserialize(d)?;
65        s.parse().map_err(serde::de::Error::custom)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn compatibility_policy() {
75        let reader = SchemaVersion::new(1, 2);
76        assert!(reader.can_read(&SchemaVersion::new(1, 0)));
77        assert!(reader.can_read(&SchemaVersion::new(1, 2)));
78        assert!(!reader.can_read(&SchemaVersion::new(1, 3)));
79        assert!(!reader.can_read(&SchemaVersion::new(2, 0)));
80        assert!(!reader.can_read(&SchemaVersion::new(0, 9)));
81    }
82
83    #[test]
84    fn parses() {
85        assert_eq!("1.0".parse::<SchemaVersion>().unwrap(), SchemaVersion::new(1, 0));
86        assert!("1".parse::<SchemaVersion>().is_err());
87        assert!("01.0".parse::<SchemaVersion>().is_err());
88        assert!("1.x".parse::<SchemaVersion>().is_err());
89    }
90}