rustapi_openapi/versioning/
version.rs1use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::fmt;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct ApiVersion {
19 pub major: u32,
21 pub minor: u32,
23 pub patch: u32,
25}
26
27impl ApiVersion {
28 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
30 Self {
31 major,
32 minor,
33 patch,
34 }
35 }
36
37 pub fn major(major: u32) -> Self {
39 Self {
40 major,
41 minor: 0,
42 patch: 0,
43 }
44 }
45
46 pub fn v1() -> Self {
48 Self::new(1, 0, 0)
49 }
50
51 pub fn v2() -> Self {
53 Self::new(2, 0, 0)
54 }
55
56 pub fn v3() -> Self {
58 Self::new(3, 0, 0)
59 }
60
61 pub fn is_compatible_with(&self, other: &ApiVersion) -> bool {
66 self.major == other.major
67 }
68
69 pub fn satisfies(&self, range: &VersionRange) -> bool {
71 range.contains(self)
72 }
73
74 pub fn as_path_segment(&self) -> String {
76 if self.minor == 0 && self.patch == 0 {
77 format!("v{}", self.major)
78 } else if self.patch == 0 {
79 format!("v{}.{}", self.major, self.minor)
80 } else {
81 format!("v{}.{}.{}", self.major, self.minor, self.patch)
82 }
83 }
84}
85
86impl Default for ApiVersion {
87 fn default() -> Self {
88 Self::v1()
89 }
90}
91
92impl fmt::Display for ApiVersion {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
95 }
96}
97
98impl FromStr for ApiVersion {
99 type Err = VersionParseError;
100
101 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 let s = s
104 .strip_prefix('v')
105 .or_else(|| s.strip_prefix('V'))
106 .unwrap_or(s);
107
108 let parts: Vec<&str> = s.split('.').collect();
109
110 match parts.len() {
111 1 => {
112 let major = parts[0]
113 .parse()
114 .map_err(|_| VersionParseError::InvalidNumber)?;
115 Ok(ApiVersion::major(major))
116 }
117 2 => {
118 let major = parts[0]
119 .parse()
120 .map_err(|_| VersionParseError::InvalidNumber)?;
121 let minor = parts[1]
122 .parse()
123 .map_err(|_| VersionParseError::InvalidNumber)?;
124 Ok(ApiVersion::new(major, minor, 0))
125 }
126 3 => {
127 let major = parts[0]
128 .parse()
129 .map_err(|_| VersionParseError::InvalidNumber)?;
130 let minor = parts[1]
131 .parse()
132 .map_err(|_| VersionParseError::InvalidNumber)?;
133 let patch = parts[2]
134 .parse()
135 .map_err(|_| VersionParseError::InvalidNumber)?;
136 Ok(ApiVersion::new(major, minor, patch))
137 }
138 _ => Err(VersionParseError::InvalidFormat),
139 }
140 }
141}
142
143impl PartialOrd for ApiVersion {
144 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
145 Some(self.cmp(other))
146 }
147}
148
149impl Ord for ApiVersion {
150 fn cmp(&self, other: &Self) -> Ordering {
151 match self.major.cmp(&other.major) {
152 Ordering::Equal => match self.minor.cmp(&other.minor) {
153 Ordering::Equal => self.patch.cmp(&other.patch),
154 ord => ord,
155 },
156 ord => ord,
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum VersionParseError {
164 InvalidNumber,
166 InvalidFormat,
168 Empty,
170}
171
172impl fmt::Display for VersionParseError {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 match self {
175 Self::InvalidNumber => write!(f, "invalid number in version"),
176 Self::InvalidFormat => write!(f, "invalid version format"),
177 Self::Empty => write!(f, "empty version string"),
178 }
179 }
180}
181
182impl std::error::Error for VersionParseError {}
183
184#[derive(Debug, Clone)]
186pub struct VersionRange {
187 pub min: Option<ApiVersion>,
189 pub max: Option<ApiVersion>,
191 pub excluded: Vec<ApiVersion>,
193}
194
195impl VersionRange {
196 pub fn any() -> Self {
198 Self {
199 min: None,
200 max: None,
201 excluded: Vec::new(),
202 }
203 }
204
205 pub fn major(version: u32) -> Self {
207 Self {
208 min: Some(ApiVersion::new(version, 0, 0)),
209 max: Some(ApiVersion::new(version, u32::MAX, u32::MAX)),
210 excluded: Vec::new(),
211 }
212 }
213
214 pub fn from(version: ApiVersion) -> Self {
216 Self {
217 min: Some(version),
218 max: None,
219 excluded: Vec::new(),
220 }
221 }
222
223 pub fn until(version: ApiVersion) -> Self {
225 Self {
226 min: None,
227 max: Some(version),
228 excluded: Vec::new(),
229 }
230 }
231
232 pub fn between(min: ApiVersion, max: ApiVersion) -> Self {
234 Self {
235 min: Some(min),
236 max: Some(max),
237 excluded: Vec::new(),
238 }
239 }
240
241 pub fn exact(version: ApiVersion) -> Self {
243 Self {
244 min: Some(version),
245 max: Some(version),
246 excluded: Vec::new(),
247 }
248 }
249
250 pub fn exclude(mut self, version: ApiVersion) -> Self {
252 self.excluded.push(version);
253 self
254 }
255
256 pub fn contains(&self, version: &ApiVersion) -> bool {
258 if self.excluded.contains(version) {
260 return false;
261 }
262
263 if let Some(min) = &self.min {
265 if version < min {
266 return false;
267 }
268 }
269
270 if let Some(max) = &self.max {
272 if version > max {
273 return false;
274 }
275 }
276
277 true
278 }
279}
280
281impl Default for VersionRange {
282 fn default() -> Self {
283 Self::any()
284 }
285}
286
287pub trait VersionMatcher: Send + Sync {
289 fn matches(&self, version: &ApiVersion) -> bool;
291
292 fn priority(&self) -> i32 {
294 0
295 }
296}
297
298impl VersionMatcher for ApiVersion {
299 fn matches(&self, version: &ApiVersion) -> bool {
300 self == version
301 }
302}
303
304impl VersionMatcher for VersionRange {
305 fn matches(&self, version: &ApiVersion) -> bool {
306 self.contains(version)
307 }
308}
309
310pub struct MajorVersionMatcher {
312 major: u32,
313}
314
315impl MajorVersionMatcher {
316 pub fn new(major: u32) -> Self {
318 Self { major }
319 }
320}
321
322impl VersionMatcher for MajorVersionMatcher {
323 fn matches(&self, version: &ApiVersion) -> bool {
324 version.major == self.major
325 }
326}
327
328pub struct AnyVersionMatcher;
330
331impl VersionMatcher for AnyVersionMatcher {
332 fn matches(&self, _version: &ApiVersion) -> bool {
333 true
334 }
335
336 fn priority(&self) -> i32 {
337 -1 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_version_parsing() {
347 assert_eq!("1".parse::<ApiVersion>().unwrap(), ApiVersion::major(1));
348 assert_eq!("v1".parse::<ApiVersion>().unwrap(), ApiVersion::major(1));
349 assert_eq!(
350 "1.2".parse::<ApiVersion>().unwrap(),
351 ApiVersion::new(1, 2, 0)
352 );
353 assert_eq!(
354 "v1.2.3".parse::<ApiVersion>().unwrap(),
355 ApiVersion::new(1, 2, 3)
356 );
357 assert_eq!("V2".parse::<ApiVersion>().unwrap(), ApiVersion::major(2));
358 }
359
360 #[test]
361 fn test_version_parsing_errors() {
362 assert!("".parse::<ApiVersion>().is_err());
363 assert!("x".parse::<ApiVersion>().is_err());
364 assert!("1.2.3.4".parse::<ApiVersion>().is_err());
365 assert!("v".parse::<ApiVersion>().is_err());
366 }
367
368 #[test]
369 fn test_version_comparison() {
370 assert!(ApiVersion::new(2, 0, 0) > ApiVersion::new(1, 0, 0));
371 assert!(ApiVersion::new(1, 1, 0) > ApiVersion::new(1, 0, 0));
372 assert!(ApiVersion::new(1, 0, 1) > ApiVersion::new(1, 0, 0));
373 assert!(ApiVersion::new(1, 0, 0) == ApiVersion::new(1, 0, 0));
374 }
375
376 #[test]
377 fn test_version_compatibility() {
378 let v1_0 = ApiVersion::new(1, 0, 0);
379 let v1_1 = ApiVersion::new(1, 1, 0);
380 let v2_0 = ApiVersion::new(2, 0, 0);
381
382 assert!(v1_0.is_compatible_with(&v1_1));
383 assert!(v1_1.is_compatible_with(&v1_0));
384 assert!(!v1_0.is_compatible_with(&v2_0));
385 }
386
387 #[test]
388 fn test_version_as_path_segment() {
389 assert_eq!(ApiVersion::major(1).as_path_segment(), "v1");
390 assert_eq!(ApiVersion::new(1, 2, 0).as_path_segment(), "v1.2");
391 assert_eq!(ApiVersion::new(1, 2, 3).as_path_segment(), "v1.2.3");
392 }
393
394 #[test]
395 fn test_version_range_contains() {
396 let range = VersionRange::between(ApiVersion::new(1, 0, 0), ApiVersion::new(2, 0, 0));
397
398 assert!(range.contains(&ApiVersion::new(1, 0, 0)));
399 assert!(range.contains(&ApiVersion::new(1, 5, 0)));
400 assert!(range.contains(&ApiVersion::new(2, 0, 0)));
401 assert!(!range.contains(&ApiVersion::new(0, 9, 0)));
402 assert!(!range.contains(&ApiVersion::new(2, 0, 1)));
403 }
404
405 #[test]
406 fn test_version_range_exclude() {
407 let range = VersionRange::major(1).exclude(ApiVersion::new(1, 5, 0));
408
409 assert!(range.contains(&ApiVersion::new(1, 0, 0)));
410 assert!(range.contains(&ApiVersion::new(1, 4, 0)));
411 assert!(!range.contains(&ApiVersion::new(1, 5, 0)));
412 assert!(range.contains(&ApiVersion::new(1, 6, 0)));
413 }
414
415 #[test]
416 fn test_version_display() {
417 assert_eq!(ApiVersion::new(1, 2, 3).to_string(), "1.2.3");
418 }
419}