1use std::cmp::Ordering;
7use std::fmt;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct SchemaVersion {
28 pub major: u16,
30 pub minor: u16,
32}
33
34impl SchemaVersion {
35 pub const fn new(major: u16, minor: u16) -> Self {
37 Self { major, minor }
38 }
39
40 pub fn parse(s: &str) -> Option<Self> {
48 let s = s.strip_prefix('v').unwrap_or(s);
49
50 if let Some((major_str, minor_str)) = s.split_once('.') {
51 let major = major_str.parse().ok()?;
52 let minor = minor_str.parse().ok()?;
53 Some(Self::new(major, minor))
54 } else {
55 let major = s.parse().ok()?;
56 Some(Self::new(major, 0))
57 }
58 }
59
60 pub fn is_compatible_with(&self, other: &Self) -> bool {
65 self.major == other.major && self.minor >= other.minor
66 }
67
68 pub fn is_successor_of(&self, other: &Self) -> bool {
70 (self.major == other.major && self.minor == other.minor + 1)
71 || (self.major == other.major + 1 && self.minor == 0)
72 }
73
74 pub fn next_minor(&self) -> Self {
76 Self::new(self.major, self.minor + 1)
77 }
78
79 pub fn next_major(&self) -> Self {
81 Self::new(self.major + 1, 0)
82 }
83
84 pub fn to_version_string(&self) -> String {
86 format!("v{}.{}", self.major, self.minor)
87 }
88
89 pub fn to_short_string(&self) -> String {
91 if self.minor == 0 {
92 format!("v{}", self.major)
93 } else {
94 self.to_version_string()
95 }
96 }
97}
98
99impl Default for SchemaVersion {
100 fn default() -> Self {
101 Self::new(1, 0)
102 }
103}
104
105impl fmt::Display for SchemaVersion {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(f, "v{}.{}", self.major, self.minor)
108 }
109}
110
111impl FromStr for SchemaVersion {
112 type Err = &'static str;
113
114 fn from_str(s: &str) -> Result<Self, Self::Err> {
115 Self::parse(s).ok_or("Invalid version format")
116 }
117}
118
119impl PartialOrd for SchemaVersion {
120 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
121 Some(self.cmp(other))
122 }
123}
124
125impl Ord for SchemaVersion {
126 fn cmp(&self, other: &Self) -> Ordering {
127 match self.major.cmp(&other.major) {
128 Ordering::Equal => self.minor.cmp(&other.minor),
129 ord => ord,
130 }
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct VersionRange {
140 pub min: SchemaVersion,
142 pub max: Option<SchemaVersion>,
144}
145
146impl VersionRange {
147 pub fn from(version: SchemaVersion) -> Self {
149 Self {
150 min: version,
151 max: None,
152 }
153 }
154
155 pub fn between(min: SchemaVersion, max: SchemaVersion) -> Self {
157 Self {
158 min,
159 max: Some(max),
160 }
161 }
162
163 pub fn exact(version: SchemaVersion) -> Self {
165 Self {
166 min: version,
167 max: Some(version),
168 }
169 }
170
171 pub fn contains(&self, version: SchemaVersion) -> bool {
173 if version < self.min {
174 return false;
175 }
176 match self.max {
177 Some(max) => version <= max,
178 None => true,
179 }
180 }
181
182 pub fn overlaps(&self, other: &VersionRange) -> bool {
184 let self_max = self.max.unwrap_or(SchemaVersion::new(u16::MAX, u16::MAX));
186 let other_max = other.max.unwrap_or(SchemaVersion::new(u16::MAX, u16::MAX));
187
188 self.min <= other_max && other.min <= self_max
189 }
190}
191
192impl fmt::Display for VersionRange {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self.max {
195 Some(max) if max == self.min => write!(f, "{}", self.min),
196 Some(max) => write!(f, "{}-{}", self.min, max),
197 None => write!(f, "{}+", self.min),
198 }
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ChangeKind {
208 AddOptionalField,
210 AddRequiredField,
212 RemoveField,
214 ChangeFieldType,
216 RenameField,
218 DeprecateField,
220 ChangeDefault,
222 MakeOptional,
224 MakeRequired,
226}
227
228impl ChangeKind {
229 pub fn is_breaking(&self) -> bool {
234 matches!(
235 self,
236 Self::AddRequiredField
237 | Self::RemoveField
238 | Self::ChangeFieldType
239 | Self::RenameField
240 | Self::MakeRequired
241 )
242 }
243
244 pub fn description(&self) -> &'static str {
246 match self {
247 Self::AddOptionalField => "Added optional field",
248 Self::AddRequiredField => "Added required field",
249 Self::RemoveField => "Removed field",
250 Self::ChangeFieldType => "Changed field type",
251 Self::RenameField => "Renamed field",
252 Self::DeprecateField => "Deprecated field",
253 Self::ChangeDefault => "Changed default value",
254 Self::MakeOptional => "Made field optional",
255 Self::MakeRequired => "Made field required",
256 }
257 }
258}
259
260impl fmt::Display for ChangeKind {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 write!(f, "{}", self.description())
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn version_creation() {
272 let v = SchemaVersion::new(1, 2);
273 assert_eq!(v.major, 1);
274 assert_eq!(v.minor, 2);
275 }
276
277 #[test]
278 fn version_parsing() {
279 assert_eq!(SchemaVersion::parse("v1"), Some(SchemaVersion::new(1, 0)));
280 assert_eq!(SchemaVersion::parse("v1.2"), Some(SchemaVersion::new(1, 2)));
281 assert_eq!(SchemaVersion::parse("1.0"), Some(SchemaVersion::new(1, 0)));
282 assert_eq!(SchemaVersion::parse("2"), Some(SchemaVersion::new(2, 0)));
283 assert_eq!(SchemaVersion::parse("invalid"), None);
284 assert_eq!(SchemaVersion::parse("v"), None);
285 }
286
287 #[test]
288 fn version_display() {
289 assert_eq!(SchemaVersion::new(1, 0).to_string(), "v1.0");
290 assert_eq!(SchemaVersion::new(2, 3).to_string(), "v2.3");
291 assert_eq!(SchemaVersion::new(1, 0).to_short_string(), "v1");
292 assert_eq!(SchemaVersion::new(1, 1).to_short_string(), "v1.1");
293 }
294
295 #[test]
296 fn version_compatibility() {
297 let v1_0 = SchemaVersion::new(1, 0);
298 let v1_1 = SchemaVersion::new(1, 1);
299 let v2_0 = SchemaVersion::new(2, 0);
300
301 assert!(v1_1.is_compatible_with(&v1_0));
303
304 assert!(!v1_0.is_compatible_with(&v1_1));
306
307 assert!(v1_0.is_compatible_with(&v1_0));
309
310 assert!(!v2_0.is_compatible_with(&v1_0));
312 assert!(!v1_0.is_compatible_with(&v2_0));
313 }
314
315 #[test]
316 fn version_ordering() {
317 let v1_0 = SchemaVersion::new(1, 0);
318 let v1_1 = SchemaVersion::new(1, 1);
319 let v2_0 = SchemaVersion::new(2, 0);
320
321 assert!(v1_0 < v1_1);
322 assert!(v1_1 < v2_0);
323 assert!(v1_0 < v2_0);
324
325 let mut versions = vec![v2_0, v1_0, v1_1];
326 versions.sort();
327 assert_eq!(versions, vec![v1_0, v1_1, v2_0]);
328 }
329
330 #[test]
331 fn version_successor() {
332 let v1_0 = SchemaVersion::new(1, 0);
333 let v1_1 = SchemaVersion::new(1, 1);
334 let v2_0 = SchemaVersion::new(2, 0);
335
336 assert!(v1_1.is_successor_of(&v1_0));
337 assert!(v2_0.is_successor_of(&v1_1));
338 assert!(v2_0.is_successor_of(&v1_0));
339 assert!(!v1_0.is_successor_of(&v1_1));
340 }
341
342 #[test]
343 fn version_range_contains() {
344 let range = VersionRange::between(SchemaVersion::new(1, 0), SchemaVersion::new(1, 5));
345
346 assert!(range.contains(SchemaVersion::new(1, 0)));
347 assert!(range.contains(SchemaVersion::new(1, 3)));
348 assert!(range.contains(SchemaVersion::new(1, 5)));
349 assert!(!range.contains(SchemaVersion::new(0, 9)));
350 assert!(!range.contains(SchemaVersion::new(1, 6)));
351 assert!(!range.contains(SchemaVersion::new(2, 0)));
352 }
353
354 #[test]
355 fn version_range_from() {
356 let range = VersionRange::from(SchemaVersion::new(1, 0));
357
358 assert!(!range.contains(SchemaVersion::new(0, 9)));
359 assert!(range.contains(SchemaVersion::new(1, 0)));
360 assert!(range.contains(SchemaVersion::new(2, 5)));
361 assert!(range.contains(SchemaVersion::new(100, 0)));
362 }
363
364 #[test]
365 fn version_range_exact() {
366 let range = VersionRange::exact(SchemaVersion::new(1, 2));
367
368 assert!(!range.contains(SchemaVersion::new(1, 1)));
369 assert!(range.contains(SchemaVersion::new(1, 2)));
370 assert!(!range.contains(SchemaVersion::new(1, 3)));
371 }
372
373 #[test]
374 fn version_range_overlaps() {
375 let range1 = VersionRange::between(SchemaVersion::new(1, 0), SchemaVersion::new(1, 5));
376 let range2 = VersionRange::between(SchemaVersion::new(1, 3), SchemaVersion::new(2, 0));
377 let range3 = VersionRange::between(SchemaVersion::new(2, 0), SchemaVersion::new(3, 0));
378
379 assert!(range1.overlaps(&range2)); assert!(range2.overlaps(&range3)); assert!(!range1.overlaps(&range3)); }
383
384 #[test]
385 fn change_kind_breaking() {
386 assert!(!ChangeKind::AddOptionalField.is_breaking());
387 assert!(ChangeKind::AddRequiredField.is_breaking());
388 assert!(ChangeKind::RemoveField.is_breaking());
389 assert!(ChangeKind::ChangeFieldType.is_breaking());
390 assert!(ChangeKind::RenameField.is_breaking());
391 assert!(!ChangeKind::DeprecateField.is_breaking());
392 assert!(!ChangeKind::ChangeDefault.is_breaking());
393 assert!(!ChangeKind::MakeOptional.is_breaking());
394 assert!(ChangeKind::MakeRequired.is_breaking());
395 }
396}