1#[cfg(all(not(feature = "std"), feature = "alloc"))]
25use alloc::{
26 string::{String, ToString},
27 vec::Vec,
28};
29use core::fmt;
30#[cfg(any(feature = "std", feature = "alloc"))]
31use core::str::FromStr;
32#[cfg(feature = "serde")]
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46#[cfg_attr(feature = "serde", derive(Serialize))]
47#[cfg_attr(feature = "serde", serde(into = "String"))]
48#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
49#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
50pub enum ApiVersion {
51 Simple(u32),
53 Semver(SemverTriple),
55 Date(u16, u8, u8),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
62#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
63#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
64pub struct SemverTriple(pub u32, pub u32, pub u32);
65
66impl fmt::Display for SemverTriple {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "{}.{}.{}", self.0, self.1, self.2)
73 }
74}
75
76impl fmt::Display for ApiVersion {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Simple(n) => write!(f, "v{n}"),
80 Self::Semver(t) => write!(f, "{t}"),
81 Self::Date(y, m, d) => write!(f, "{y:04}-{m:02}-{d:02}"),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
93pub struct ApiVersionParseError(
94 #[cfg(any(feature = "std", feature = "alloc"))] pub String,
95 #[cfg(not(any(feature = "std", feature = "alloc")))] pub &'static str,
96);
97
98impl fmt::Display for ApiVersionParseError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "invalid API version: {}", self.0)
101 }
102}
103
104#[cfg(feature = "std")]
105impl std::error::Error for ApiVersionParseError {}
106
107#[cfg(any(feature = "std", feature = "alloc"))]
108impl FromStr for ApiVersion {
109 type Err = ApiVersionParseError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 if let Some(rest) = s.strip_prefix(['v', 'V']) {
124 let n: u32 = rest.parse().map_err(|_| ApiVersionParseError(s.into()))?;
125 return Ok(Self::Simple(n));
126 }
127
128 if s.len() == 10 && s.as_bytes().get(4) == Some(&b'-') && s.as_bytes().get(7) == Some(&b'-')
130 {
131 let year: u16 = s[..4].parse().map_err(|_| ApiVersionParseError(s.into()))?;
132 let month: u8 = s[5..7]
133 .parse()
134 .map_err(|_| ApiVersionParseError(s.into()))?;
135 let day: u8 = s[8..10]
136 .parse()
137 .map_err(|_| ApiVersionParseError(s.into()))?;
138 if (1..=12).contains(&month) && (1..=31).contains(&day) {
139 return Ok(Self::Date(year, month, day));
140 }
141 return Err(ApiVersionParseError(s.into()));
142 }
143
144 let parts: Vec<&str> = s.splitn(4, '.').collect();
146 if parts.len() == 3 {
147 let maj: u32 = parts[0]
148 .parse()
149 .map_err(|_| ApiVersionParseError(s.into()))?;
150 let min: u32 = parts[1]
151 .parse()
152 .map_err(|_| ApiVersionParseError(s.into()))?;
153 let pat: u32 = parts[2]
154 .parse()
155 .map_err(|_| ApiVersionParseError(s.into()))?;
156 return Ok(Self::Semver(SemverTriple(maj, min, pat)));
157 }
158
159 Err(ApiVersionParseError(s.into()))
160 }
161}
162
163#[cfg(any(feature = "std", feature = "alloc"))]
169impl From<ApiVersion> for String {
170 fn from(v: ApiVersion) -> Self {
171 v.to_string()
172 }
173}
174
175#[cfg(feature = "serde")]
176#[cfg(any(feature = "std", feature = "alloc"))]
177impl<'de> Deserialize<'de> for ApiVersion {
178 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
179 let s = String::deserialize(deserializer)?;
180 s.parse::<Self>().map_err(serde::de::Error::custom)
181 }
182}
183
184pub const ACCEPT_VERSION: &str = "Accept-Version";
190
191pub const CONTENT_VERSION: &str = "Content-Version";
193
194impl ApiVersion {
195 #[must_use]
207 #[cfg(any(feature = "std", feature = "alloc"))]
208 pub fn header_value(&self) -> String {
209 self.to_string()
210 }
211
212 #[cfg(feature = "http")]
219 pub fn inject_content_version(
220 &self,
221 headers: &mut http::HeaderMap,
222 ) -> Result<(), http::header::InvalidHeaderValue> {
223 #[cfg(not(feature = "std"))]
224 use alloc::string::ToString;
225 use http::header::HeaderValue;
226 let val = HeaderValue::from_str(&self.to_string())?;
227 headers.insert(
228 http::header::HeaderName::from_static("content-version"),
229 val,
230 );
231 Ok(())
232 }
233}
234
235#[cfg(all(feature = "axum", any(feature = "std", feature = "alloc")))]
244impl<S: Send + Sync> axum::extract::FromRequestParts<S> for ApiVersion {
245 type Rejection = crate::error::ApiError;
246
247 async fn from_request_parts(
248 parts: &mut axum::http::request::Parts,
249 _state: &S,
250 ) -> Result<Self, Self::Rejection> {
251 if let Some(val) = parts.headers.get("x-api-version") {
253 let s = val.to_str().map_err(|_| {
254 crate::error::ApiError::bad_request("header x-api-version contains non-UTF-8 bytes")
255 })?;
256 return s.parse::<Self>().map_err(|e| {
257 crate::error::ApiError::bad_request(format!("invalid X-Api-Version: {e}"))
258 });
259 }
260 if let Some(query) = parts.uri.query() {
262 for pair in query.split('&') {
263 if let Some(v) = pair.strip_prefix("v=") {
264 return v.parse::<Self>().map_err(|e| {
265 crate::error::ApiError::bad_request(format!("invalid v= query param: {e}"))
266 });
267 }
268 }
269 }
270 Err(crate::error::ApiError::bad_request(
271 "missing api version: provide X-Api-Version header or v= query parameter",
272 ))
273 }
274}
275
276#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn parse_simple() {
286 let v: ApiVersion = "v1".parse().unwrap();
287 assert_eq!(v, ApiVersion::Simple(1));
288 assert_eq!(v.to_string(), "v1");
289 }
290
291 #[test]
292 fn parse_simple_uppercase() {
293 let v: ApiVersion = "V42".parse().unwrap();
294 assert_eq!(v, ApiVersion::Simple(42));
295 }
296
297 #[test]
298 fn parse_semver() {
299 let v: ApiVersion = "1.2.3".parse().unwrap();
300 assert_eq!(v, ApiVersion::Semver(SemverTriple(1, 2, 3)));
301 assert_eq!(v.to_string(), "1.2.3");
302 }
303
304 #[test]
305 fn parse_date() {
306 let v: ApiVersion = "2024-06-01".parse().unwrap();
307 assert_eq!(v, ApiVersion::Date(2024, 6, 1));
308 assert_eq!(v.to_string(), "2024-06-01");
309 }
310
311 #[test]
312 fn parse_invalid() {
313 assert!("nope".parse::<ApiVersion>().is_err());
314 assert!("1.2".parse::<ApiVersion>().is_err());
315 assert!("2024-13-01".parse::<ApiVersion>().is_err());
316 }
317
318 #[test]
319 fn ordering_simple() {
320 let v1: ApiVersion = "v1".parse().unwrap();
321 let v2: ApiVersion = "v2".parse().unwrap();
322 assert!(v1 < v2);
323 }
324
325 #[test]
326 fn ordering_semver() {
327 let a: ApiVersion = "1.0.0".parse().unwrap();
328 let b: ApiVersion = "1.0.1".parse().unwrap();
329 let c: ApiVersion = "2.0.0".parse().unwrap();
330 assert!(a < b);
331 assert!(b < c);
332 }
333
334 #[test]
335 fn ordering_date() {
336 let a: ApiVersion = "2024-01-01".parse().unwrap();
337 let b: ApiVersion = "2024-06-01".parse().unwrap();
338 assert!(a < b);
339 }
340
341 #[cfg(any(feature = "std", feature = "alloc"))]
342 #[test]
343 fn header_value() {
344 let v = ApiVersion::Date(2024, 6, 1);
345 assert_eq!(v.header_value(), "2024-06-01");
346 }
347
348 #[cfg(feature = "serde")]
349 #[test]
350 fn serde_round_trip_simple() {
351 let v = ApiVersion::Simple(3);
352 let s = serde_json::to_value(&v).unwrap();
354 assert_eq!(s, serde_json::json!("v3"));
355 let back: ApiVersion = serde_json::from_value(s).unwrap();
356 assert_eq!(back, v);
357 }
358
359 #[cfg(feature = "serde")]
360 #[test]
361 fn serde_round_trip_semver() {
362 let v = ApiVersion::Semver(SemverTriple(1, 2, 3));
363 let s = serde_json::to_value(&v).unwrap();
365 assert_eq!(s, serde_json::json!("1.2.3"));
366 let back: ApiVersion = serde_json::from_value(s).unwrap();
367 assert_eq!(back, v);
368 }
369
370 #[test]
371 fn semver_triple_display() {
372 let t = SemverTriple(2, 10, 0);
373 assert_eq!(t.to_string(), "2.10.0");
374 }
375
376 #[test]
377 fn api_version_parse_error_display() {
378 let err = ApiVersionParseError("bad".into());
379 let s = err.to_string();
380 assert!(s.contains("invalid API version"));
381 assert!(s.contains("bad"));
382 }
383
384 #[test]
385 fn ordering_cross_variant() {
386 let simple: ApiVersion = "v1".parse().unwrap();
388 let semver: ApiVersion = "1.0.0".parse().unwrap();
389 let date: ApiVersion = "2024-01-01".parse().unwrap();
390 assert!(simple < semver);
391 assert!(semver < date);
392 }
393
394 #[cfg(any(feature = "std", feature = "alloc"))]
395 #[test]
396 fn header_value_simple() {
397 let v = ApiVersion::Simple(5);
398 assert_eq!(v.header_value(), "v5");
399 }
400
401 #[cfg(any(feature = "std", feature = "alloc"))]
402 #[test]
403 fn header_value_semver() {
404 let v = ApiVersion::Semver(SemverTriple(1, 2, 3));
405 assert_eq!(v.header_value(), "1.2.3");
406 }
407
408 #[test]
409 fn parse_date_invalid_day_zero() {
410 assert!("2024-01-00".parse::<ApiVersion>().is_err());
411 }
412
413 #[test]
414 fn parse_date_invalid_month_zero() {
415 assert!("2024-00-01".parse::<ApiVersion>().is_err());
416 }
417
418 #[test]
419 fn parse_semver_bad_component() {
420 assert!("1.x.3".parse::<ApiVersion>().is_err());
421 }
422
423 #[test]
424 fn parse_simple_bad_number() {
425 assert!("vabc".parse::<ApiVersion>().is_err());
426 }
427
428 #[test]
429 fn display_date_pads_correctly() {
430 let v = ApiVersion::Date(2024, 1, 5);
431 assert_eq!(v.to_string(), "2024-01-05");
432 }
433
434 #[cfg(feature = "serde")]
435 #[test]
436 fn serde_round_trip_date() {
437 let v = ApiVersion::Date(2024, 6, 1);
438 let json = serde_json::to_value(&v).unwrap();
440 assert_eq!(json, serde_json::json!("2024-06-01"));
441 let back: ApiVersion = serde_json::from_value(json).unwrap();
442 assert_eq!(back, v);
443 }
444
445 #[cfg(feature = "http")]
446 #[test]
447 fn inject_content_version_header() {
448 let v = ApiVersion::Simple(3);
449 let mut headers = http::HeaderMap::new();
450 v.inject_content_version(&mut headers).unwrap();
451 assert_eq!(headers["content-version"], "v3");
452 }
453
454 #[cfg(feature = "std")]
455 #[test]
456 fn api_version_parse_error_is_std_error() {
457 let err = ApiVersionParseError("oops".into());
459 let boxed: Box<dyn std::error::Error> = Box::new(err);
460 assert!(boxed.source().is_none());
461 }
462
463 #[test]
464 fn semver_triple_ordering() {
465 let a = SemverTriple(1, 0, 0);
466 let b = SemverTriple(1, 1, 0);
467 let c = SemverTriple(2, 0, 0);
468 assert!(a < b);
469 assert!(b < c);
470 assert!(a < c);
471 assert_eq!(a, SemverTriple(1, 0, 0));
472 }
473
474 #[test]
475 fn api_version_parse_error_clone_and_eq() {
476 let err = ApiVersionParseError("bad-version".into());
477 let cloned = err.clone();
478 assert_eq!(err, cloned);
479 }
480
481 #[test]
482 fn parse_date_invalid_year_non_numeric() {
483 assert!("abcd-01-01".parse::<ApiVersion>().is_err());
486 }
487
488 #[test]
489 fn parse_date_invalid_day_non_numeric() {
490 assert!("2024-01-xx".parse::<ApiVersion>().is_err());
492 }
493
494 #[test]
495 fn parse_date_invalid_month_non_numeric() {
496 assert!("2024-xx-01".parse::<ApiVersion>().is_err());
498 }
499
500 #[test]
501 fn parse_semver_bad_major() {
502 assert!("x.1.3".parse::<ApiVersion>().is_err());
504 }
505
506 #[test]
507 fn parse_semver_bad_patch() {
508 assert!("1.2.x".parse::<ApiVersion>().is_err());
510 }
511
512 #[test]
513 fn parse_semver_too_many_parts() {
514 assert!("1.2.3.4".parse::<ApiVersion>().is_err());
517 }
518
519 #[test]
520 fn hash_semver_triple() {
521 use core::hash::{Hash, Hasher};
522 use std::collections::hash_map::DefaultHasher;
523 let mut h1 = DefaultHasher::new();
524 let mut h2 = DefaultHasher::new();
525 SemverTriple(1, 2, 3).hash(&mut h1);
526 SemverTriple(1, 2, 3).hash(&mut h2);
527 assert_eq!(h1.finish(), h2.finish());
528 }
529
530 #[test]
531 fn hash_api_version() {
532 use core::hash::{Hash, Hasher};
533 use std::collections::hash_map::DefaultHasher;
534 let mut h = DefaultHasher::new();
535 ApiVersion::Simple(1).hash(&mut h);
536 let _ = h.finish();
537 }
538
539 #[test]
540 fn api_version_in_hashset() {
541 use std::collections::HashSet;
542 let mut set = HashSet::new();
543 set.insert(ApiVersion::Simple(1));
544 set.insert(ApiVersion::Semver(SemverTriple(1, 0, 0)));
545 set.insert(ApiVersion::Date(2024, 1, 1));
546 assert_eq!(set.len(), 3);
547 assert!(set.contains(&ApiVersion::Simple(1)));
548 }
549
550 #[test]
551 fn semver_triple_in_hashset() {
552 use std::collections::HashSet;
553 let mut set = HashSet::new();
554 set.insert(SemverTriple(1, 2, 3));
555 assert!(set.contains(&SemverTriple(1, 2, 3)));
556 }
557
558 #[test]
559 fn api_version_parse_error_in_hashset() {
560 let e1 = ApiVersionParseError("x".into());
562 let e2 = e1.clone();
563 assert_eq!(e1, e2);
564 assert_ne!(e1, ApiVersionParseError("y".into()));
565 }
566
567 #[test]
568 fn semver_triple_ord_cmp() {
569 use core::cmp::Ordering;
570 let a = SemverTriple(1, 0, 0);
571 let b = SemverTriple(2, 0, 0);
572 assert_eq!(a.cmp(&b), Ordering::Less);
573 assert_eq!(b.cmp(&a), Ordering::Greater);
574 assert_eq!(a.cmp(&a), Ordering::Equal);
575 }
576
577 #[test]
578 fn api_version_ord_cmp() {
579 use core::cmp::Ordering;
580 let a = ApiVersion::Simple(1);
581 let b = ApiVersion::Simple(2);
582 assert_eq!(a.cmp(&b), Ordering::Less);
583 assert_eq!(b.cmp(&a), Ordering::Greater);
584 assert_eq!(a.cmp(&a), Ordering::Equal);
585 }
586
587 #[test]
588 fn api_version_parse_error_eq() {
589 use core::cmp::PartialEq;
590 let e1 = ApiVersionParseError("a".into());
591 let e2 = ApiVersionParseError("a".into());
592 let e3 = ApiVersionParseError("b".into());
593 assert!(e1.eq(&e2));
594 assert!(!e1.eq(&e3));
595 }
596
597 #[test]
598 fn api_version_clone_all_variants() {
599 let simple = ApiVersion::Simple(1);
600 let semver = ApiVersion::Semver(SemverTriple(1, 2, 3));
601 let date = ApiVersion::Date(2024, 6, 1);
602 assert_eq!(simple.clone(), simple);
603 assert_eq!(semver.clone(), semver);
604 assert_eq!(date.clone(), date);
605 }
606
607 #[test]
608 fn semver_triple_clone_and_copy() {
609 let t = SemverTriple(1, 2, 3);
610 let cloned = t; assert_eq!(t, cloned);
612 assert_eq!(t.clone(), cloned);
613 }
614
615 #[cfg(feature = "serde")]
616 #[test]
617 fn serde_round_trip_semver_triple() {
618 let t = SemverTriple(3, 14, 159);
619 let s = serde_json::to_value(t).unwrap();
620 let back: SemverTriple = serde_json::from_value(s).unwrap();
621 assert_eq!(back, t);
622 }
623
624 #[cfg(feature = "serde")]
625 #[test]
626 fn serde_parse_error_round_trip() {
627 let e = ApiVersionParseError("bad".into());
628 let s = serde_json::to_value(&e).unwrap();
629 let back: ApiVersionParseError = serde_json::from_value(s).unwrap();
630 assert_eq!(back, e);
631 }
632
633 #[cfg(feature = "serde")]
634 #[test]
635 fn serde_api_version_invalid_string_produces_error() {
636 let result: Result<ApiVersion, _> =
638 serde_json::from_value(serde_json::json!("not-a-version"));
639 assert!(result.is_err());
640 let msg = result.unwrap_err().to_string();
641 assert!(!msg.is_empty());
642 }
643
644 #[cfg(feature = "serde")]
645 #[test]
646 fn serde_api_version_non_string_is_invalid() {
647 let result: Result<ApiVersion, _> = serde_json::from_value(serde_json::json!(true));
649 assert!(result.is_err());
650 let result2: Result<ApiVersion, _> = serde_json::from_value(serde_json::json!(42));
651 assert!(result2.is_err());
652 }
653
654 #[test]
655 fn display_fmt_via_format_macro() {
656 let s = format!("{}", SemverTriple(1, 0, 0));
659 assert_eq!(s, "1.0.0");
660 let v = format!("{}", ApiVersion::Simple(7));
661 assert_eq!(v, "v7");
662 let e = format!("{}", ApiVersionParseError("x".into()));
663 assert!(e.contains("invalid API version"));
664 }
665
666 #[test]
667 fn display_fmt_direct_write() {
668 use core::fmt::Write;
669 let mut buf = String::new();
670 write!(buf, "{}", SemverTriple(2, 3, 4)).unwrap();
671 assert_eq!(buf, "2.3.4");
672 buf.clear();
673 write!(buf, "{}", ApiVersion::Semver(SemverTriple(0, 1, 0))).unwrap();
674 assert_eq!(buf, "0.1.0");
675 buf.clear();
676 write!(buf, "{}", ApiVersionParseError("z".into())).unwrap();
677 assert!(buf.contains('z'));
678 }
679}