bloqade_lanes_bytecode_core/
version.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5/// Semantic version with major.minor components.
6///
7/// Packed as `(major << 16) | minor` for binary format compatibility (4 bytes LE).
8/// Serialized to/from JSON as a `"major.minor"` string (e.g. `"1.0"`).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct Version {
11    pub major: u16,
12    pub minor: u16,
13}
14
15impl Version {
16    pub fn new(major: u16, minor: u16) -> Self {
17        Self { major, minor }
18    }
19
20    /// Two versions are compatible if their major versions match.
21    pub fn is_compatible(&self, other: &Version) -> bool {
22        self.major == other.major
23    }
24}
25
26impl fmt::Display for Version {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(f, "{}.{}", self.major, self.minor)
29    }
30}
31
32impl From<u32> for Version {
33    fn from(v: u32) -> Self {
34        Self {
35            major: (v >> 16) as u16,
36            minor: v as u16,
37        }
38    }
39}
40
41impl From<Version> for u32 {
42    fn from(v: Version) -> u32 {
43        ((v.major as u32) << 16) | (v.minor as u32)
44    }
45}
46
47/// Serializes as a `"major.minor"` string (e.g. `"1.0"`).
48impl Serialize for Version {
49    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50        serializer.collect_str(self)
51    }
52}
53
54/// Deserializes from a `"major.minor"` string (e.g. `"1.0"`).
55impl<'de> Deserialize<'de> for Version {
56    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57        let s = String::deserialize(deserializer)?;
58        let (major, minor) = s.split_once('.').ok_or_else(|| {
59            serde::de::Error::custom(format!(
60                "invalid version string '{}': expected 'major.minor'",
61                s
62            ))
63        })?;
64        let major: u16 = major.parse().map_err(serde::de::Error::custom)?;
65        let minor: u16 = minor.parse().map_err(serde::de::Error::custom)?;
66        Ok(Version::new(major, minor))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_version_display() {
76        assert_eq!(Version::new(1, 0).to_string(), "1.0");
77        assert_eq!(Version::new(2, 3).to_string(), "2.3");
78    }
79
80    #[test]
81    fn test_version_u32_round_trip() {
82        let v = Version::new(1, 0);
83        let packed: u32 = v.into();
84        assert_eq!(packed, 0x00010000);
85        assert_eq!(Version::from(packed), v);
86
87        let v2 = Version::new(2, 5);
88        let packed2: u32 = v2.into();
89        assert_eq!(packed2, 0x00020005);
90        assert_eq!(Version::from(packed2), v2);
91    }
92
93    #[test]
94    fn test_version_from_u32_packed() {
95        let v = Version::from(1u32);
96        assert_eq!(v, Version::new(0, 1));
97    }
98
99    #[test]
100    fn test_version_is_compatible() {
101        let v1 = Version::new(1, 0);
102        let v1_1 = Version::new(1, 1);
103        let v2 = Version::new(2, 0);
104
105        assert!(v1.is_compatible(&v1_1));
106        assert!(v1_1.is_compatible(&v1));
107        assert!(!v1.is_compatible(&v2));
108    }
109
110    #[test]
111    fn test_version_serde_round_trip() {
112        let v = Version::new(1, 0);
113        let json = serde_json::to_string(&v).unwrap();
114        assert_eq!(json, r#""1.0""#);
115        let deserialized: Version = serde_json::from_str(&json).unwrap();
116        assert_eq!(v, deserialized);
117    }
118
119    #[test]
120    fn test_version_serde_round_trip_with_minor() {
121        let v = Version::new(1, 2);
122        let json = serde_json::to_string(&v).unwrap();
123        assert_eq!(json, r#""1.2""#);
124        let deserialized: Version = serde_json::from_str(&json).unwrap();
125        assert_eq!(v, deserialized);
126    }
127
128    #[test]
129    fn test_version_serde_zero_zero() {
130        let v = Version::new(0, 0);
131        let json = serde_json::to_string(&v).unwrap();
132        assert_eq!(json, r#""0.0""#);
133        let deserialized: Version = serde_json::from_str(&json).unwrap();
134        assert_eq!(v, deserialized);
135    }
136
137    #[test]
138    fn test_version_serde_invalid_no_dot() {
139        let err = serde_json::from_str::<Version>(r#""bad""#).unwrap_err();
140        assert!(err.to_string().contains("expected 'major.minor'"));
141    }
142
143    #[test]
144    fn test_version_serde_invalid_major() {
145        let err = serde_json::from_str::<Version>(r#""abc.1""#).unwrap_err();
146        assert!(err.to_string().contains("invalid digit"));
147    }
148
149    #[test]
150    fn test_version_serde_invalid_minor() {
151        let err = serde_json::from_str::<Version>(r#""1.abc""#).unwrap_err();
152        assert!(err.to_string().contains("invalid digit"));
153    }
154
155    #[test]
156    fn test_version_serde_rejects_integer() {
157        let err = serde_json::from_str::<Version>("1").unwrap_err();
158        assert!(err.to_string().contains("invalid type: integer"));
159    }
160
161    #[test]
162    fn test_version_serde_rejects_boolean() {
163        let err = serde_json::from_str::<Version>("true").unwrap_err();
164        assert!(err.to_string().contains("invalid type: boolean"));
165    }
166}