use crate::error::ConfigError;
use std::fmt;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ApiVersion {
V2024_01,
V2024_04,
V2024_07,
V2024_10,
V2025_01,
V2025_04,
V2025_07,
V2025_10,
V2026_01,
Unstable,
Custom(String),
}
impl ApiVersion {
#[must_use]
pub const fn latest() -> Self {
Self::V2026_01
}
#[must_use]
pub const fn is_stable(&self) -> bool {
!matches!(self, Self::Unstable | Self::Custom(_))
}
#[must_use]
pub fn supported_versions() -> Vec<Self> {
vec![
Self::V2025_04,
Self::V2025_07,
Self::V2025_10,
Self::V2026_01,
]
}
#[must_use]
pub const fn minimum_supported() -> Self {
Self::V2025_04
}
#[must_use]
pub fn is_supported(&self) -> bool {
match self {
Self::Unstable => true,
Self::Custom(_) => true, _ => *self >= Self::minimum_supported(),
}
}
#[must_use]
pub fn is_deprecated(&self) -> bool {
match self {
Self::Unstable => false,
Self::Custom(_) => false,
_ => *self < Self::minimum_supported(),
}
}
const fn ordinal(&self) -> u32 {
match self {
Self::V2024_01 => 1,
Self::V2024_04 => 2,
Self::V2024_07 => 3,
Self::V2024_10 => 4,
Self::V2025_01 => 5,
Self::V2025_04 => 6,
Self::V2025_07 => 7,
Self::V2025_10 => 8,
Self::V2026_01 => 9,
Self::Unstable => 100, Self::Custom(_) => 101, }
}
}
impl PartialOrd for ApiVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ApiVersion {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Custom(a), Self::Custom(b)) => a.cmp(b),
_ => self.ordinal().cmp(&other.ordinal()),
}
}
}
impl fmt::Display for ApiVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let version_str = match self {
Self::V2024_01 => "2024-01",
Self::V2024_04 => "2024-04",
Self::V2024_07 => "2024-07",
Self::V2024_10 => "2024-10",
Self::V2025_01 => "2025-01",
Self::V2025_04 => "2025-04",
Self::V2025_07 => "2025-07",
Self::V2025_10 => "2025-10",
Self::V2026_01 => "2026-01",
Self::Unstable => "unstable",
Self::Custom(s) => s,
};
f.write_str(version_str)
}
}
impl FromStr for ApiVersion {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().to_lowercase();
match s.as_str() {
"2024-01" => Ok(Self::V2024_01),
"2024-04" => Ok(Self::V2024_04),
"2024-07" => Ok(Self::V2024_07),
"2024-10" => Ok(Self::V2024_10),
"2025-01" => Ok(Self::V2025_01),
"2025-04" => Ok(Self::V2025_04),
"2025-07" => Ok(Self::V2025_07),
"2025-10" => Ok(Self::V2025_10),
"2026-01" => Ok(Self::V2026_01),
"unstable" => Ok(Self::Unstable),
_ => {
if Self::is_valid_version_format(&s) {
Ok(Self::Custom(s))
} else {
Err(ConfigError::InvalidApiVersion { version: s })
}
}
}
}
}
impl ApiVersion {
fn is_valid_version_format(s: &str) -> bool {
if s.len() != 7 {
return false;
}
let parts: Vec<&str> = s.split('-').collect();
if parts.len() != 2 {
return false;
}
let year = parts[0];
let month = parts[1];
if year.len() != 4 || month.len() != 2 {
return false;
}
if !year.chars().all(|c| c.is_ascii_digit()) {
return false;
}
matches!(month, "01" | "04" | "07" | "10")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_version_parses_known_versions() {
assert_eq!(
"2024-01".parse::<ApiVersion>().unwrap(),
ApiVersion::V2024_01
);
assert_eq!(
"2024-10".parse::<ApiVersion>().unwrap(),
ApiVersion::V2024_10
);
assert_eq!(
"2025-01".parse::<ApiVersion>().unwrap(),
ApiVersion::V2025_01
);
assert_eq!(
"unstable".parse::<ApiVersion>().unwrap(),
ApiVersion::Unstable
);
}
#[test]
fn test_api_version_display() {
assert_eq!(format!("{}", ApiVersion::V2024_01), "2024-01");
assert_eq!(format!("{}", ApiVersion::V2024_10), "2024-10");
assert_eq!(format!("{}", ApiVersion::V2026_01), "2026-01");
assert_eq!(format!("{}", ApiVersion::Unstable), "unstable");
assert_eq!(
format!("{}", ApiVersion::Custom("2026-04".to_string())),
"2026-04"
);
}
#[test]
fn test_api_version_is_stable() {
assert!(ApiVersion::V2024_01.is_stable());
assert!(ApiVersion::V2025_10.is_stable());
assert!(ApiVersion::V2026_01.is_stable());
assert!(!ApiVersion::Unstable.is_stable());
assert!(!ApiVersion::Custom("2026-04".to_string()).is_stable());
}
#[test]
fn test_api_version_latest() {
let latest = ApiVersion::latest();
assert!(latest.is_stable());
assert_eq!(latest, ApiVersion::V2026_01);
}
#[test]
fn test_api_version_parses_future_versions() {
let version: ApiVersion = "2026-04".parse().unwrap();
assert_eq!(version, ApiVersion::Custom("2026-04".to_string()));
assert!(!version.is_stable());
}
#[test]
fn test_api_version_rejects_invalid() {
assert!("invalid".parse::<ApiVersion>().is_err());
assert!("2024".parse::<ApiVersion>().is_err());
assert!("2024-1".parse::<ApiVersion>().is_err());
assert!("2024-02".parse::<ApiVersion>().is_err()); assert!("24-01".parse::<ApiVersion>().is_err());
}
#[test]
fn test_supported_versions_chronological() {
let versions = ApiVersion::supported_versions();
assert!(!versions.is_empty());
assert!(versions.contains(&ApiVersion::latest()));
for window in versions.windows(2) {
assert!(
window[0] < window[1],
"Versions should be in chronological order"
);
}
for version in &versions {
assert!(version.is_supported(), "{version} should be supported");
}
}
#[test]
fn test_minimum_supported() {
let minimum = ApiVersion::minimum_supported();
assert!(minimum.is_supported());
assert!(!minimum.is_deprecated());
assert!(ApiVersion::V2024_01.is_deprecated());
assert!(ApiVersion::V2024_04.is_deprecated());
assert!(ApiVersion::V2024_07.is_deprecated());
assert!(ApiVersion::V2024_10.is_deprecated());
assert!(ApiVersion::V2025_01.is_deprecated());
}
#[test]
fn test_is_deprecated_for_old_versions() {
assert!(ApiVersion::V2024_01.is_deprecated());
assert!(ApiVersion::V2024_04.is_deprecated());
assert!(ApiVersion::V2024_07.is_deprecated());
assert!(ApiVersion::V2024_10.is_deprecated());
assert!(ApiVersion::V2025_01.is_deprecated());
assert!(!ApiVersion::V2025_04.is_deprecated());
assert!(!ApiVersion::V2025_07.is_deprecated());
assert!(!ApiVersion::V2025_10.is_deprecated());
assert!(!ApiVersion::V2026_01.is_deprecated());
assert!(!ApiVersion::Unstable.is_deprecated());
assert!(!ApiVersion::Custom("2026-04".to_string()).is_deprecated());
}
#[test]
fn test_is_supported() {
assert!(ApiVersion::V2025_04.is_supported());
assert!(ApiVersion::V2025_07.is_supported());
assert!(ApiVersion::V2025_10.is_supported());
assert!(ApiVersion::V2026_01.is_supported());
assert!(ApiVersion::Unstable.is_supported());
assert!(ApiVersion::Custom("2026-04".to_string()).is_supported());
assert!(!ApiVersion::V2024_01.is_supported());
assert!(!ApiVersion::V2024_04.is_supported());
assert!(!ApiVersion::V2024_07.is_supported());
assert!(!ApiVersion::V2024_10.is_supported());
assert!(!ApiVersion::V2025_01.is_supported());
}
#[test]
fn test_version_ordering() {
assert!(ApiVersion::V2024_01 < ApiVersion::V2024_04);
assert!(ApiVersion::V2024_04 < ApiVersion::V2024_07);
assert!(ApiVersion::V2024_07 < ApiVersion::V2024_10);
assert!(ApiVersion::V2024_10 < ApiVersion::V2025_01);
assert!(ApiVersion::V2025_01 < ApiVersion::V2025_04);
assert!(ApiVersion::V2025_04 < ApiVersion::V2025_07);
assert!(ApiVersion::V2025_07 < ApiVersion::V2025_10);
assert!(ApiVersion::V2025_10 < ApiVersion::V2026_01);
assert!(ApiVersion::V2026_01 < ApiVersion::Unstable);
assert!(ApiVersion::Unstable < ApiVersion::Custom("2026-04".to_string()));
assert!(
ApiVersion::Custom("2026-04".to_string()) < ApiVersion::Custom("2026-07".to_string())
);
}
#[test]
fn test_version_equality() {
assert_eq!(ApiVersion::V2024_01, ApiVersion::V2024_01);
assert_ne!(ApiVersion::V2024_01, ApiVersion::V2024_04);
assert_eq!(ApiVersion::V2026_01, ApiVersion::V2026_01);
assert_eq!(
ApiVersion::Custom("2026-04".to_string()),
ApiVersion::Custom("2026-04".to_string())
);
}
}