use crate::errors::{ErrorCode, ModuleError, VersionIncompatibleError};
#[derive(Debug, Clone, PartialEq, Eq)]
struct SemVer {
major: u64,
minor: u64,
patch: u64,
prerelease: Option<String>,
}
impl SemVer {
fn to_string_repr(&self) -> String {
match &self.prerelease {
Some(pre) => format!("{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
None => format!("{}.{}.{}", self.major, self.minor, self.patch),
}
}
}
impl Ord for SemVer {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.major
.cmp(&other.major)
.then(self.minor.cmp(&other.minor))
.then(self.patch.cmp(&other.patch))
.then(match (&self.prerelease, &other.prerelease) {
(None, None) => std::cmp::Ordering::Equal,
(None, Some(_)) => std::cmp::Ordering::Greater,
(Some(_), None) => std::cmp::Ordering::Less,
(Some(a), Some(b)) => a.cmp(b),
})
}
}
impl PartialOrd for SemVer {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
fn parse_semver(version: &str) -> Option<SemVer> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return None;
}
let major = parts[0].parse::<u64>().ok()?;
let minor = parts[1].parse::<u64>().ok()?;
let (patch, prerelease) = if let Some(dash_pos) = parts[2].find('-') {
let patch_str = &parts[2][..dash_pos];
let pre_str = &parts[2][dash_pos + 1..];
if pre_str.is_empty() {
return None;
}
(patch_str.parse::<u64>().ok()?, Some(pre_str.to_string()))
} else {
(parts[2].parse::<u64>().ok()?, None)
};
Some(SemVer {
major,
minor,
patch,
prerelease,
})
}
pub fn negotiate_version(declared_version: &str, sdk_version: &str) -> Result<String, ModuleError> {
let declared = parse_semver(declared_version).ok_or_else(|| {
ModuleError::new(
ErrorCode::VersionIncompatible,
format!("Invalid declared version: {declared_version}"),
)
})?;
let sdk = parse_semver(sdk_version).ok_or_else(|| {
ModuleError::new(
ErrorCode::VersionIncompatible,
format!("Invalid SDK version: {sdk_version}"),
)
})?;
if declared.major != sdk.major {
let err = VersionIncompatibleError {
message: format!(
"Major version mismatch: declared {declared_version} vs SDK {sdk_version}"
),
};
return Err(err.to_module_error());
}
if declared.minor > sdk.minor {
let err = VersionIncompatibleError {
message: format!(
"Declared minor version {} exceeds SDK minor version {} (SDK too old)",
declared.minor, sdk.minor
),
};
return Err(err.to_module_error());
}
if declared.minor < sdk.minor {
return Ok(declared_version.to_string());
}
let effective = std::cmp::max(&declared, &sdk);
Ok(effective.to_string_repr())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_semver_basic() {
let v = parse_semver("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
assert_eq!(v.prerelease, None);
}
#[test]
fn test_parse_semver_with_prerelease() {
let v = parse_semver("1.0.0-alpha").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 0);
assert_eq!(v.prerelease, Some("alpha".to_string()));
}
#[test]
fn test_parse_semver_invalid_too_few_parts() {
assert!(parse_semver("1.2").is_none());
}
#[test]
fn test_parse_semver_invalid_too_many_parts() {
assert!(parse_semver("1.2.3.4").is_none());
}
#[test]
fn test_parse_semver_invalid_non_numeric() {
assert!(parse_semver("a.b.c").is_none());
}
#[test]
fn test_parse_semver_empty_prerelease_rejected() {
assert!(parse_semver("1.0.0-").is_none());
}
#[test]
fn test_semver_ordering_major() {
let v1 = parse_semver("1.0.0").unwrap();
let v2 = parse_semver("2.0.0").unwrap();
assert!(v1 < v2);
}
#[test]
fn test_semver_ordering_minor() {
let v1 = parse_semver("1.1.0").unwrap();
let v2 = parse_semver("1.2.0").unwrap();
assert!(v1 < v2);
}
#[test]
fn test_semver_ordering_patch() {
let v1 = parse_semver("1.0.1").unwrap();
let v2 = parse_semver("1.0.2").unwrap();
assert!(v1 < v2);
}
#[test]
fn test_semver_prerelease_less_than_release() {
let pre = parse_semver("1.0.0-alpha").unwrap();
let rel = parse_semver("1.0.0").unwrap();
assert!(pre < rel);
}
#[test]
fn test_semver_prerelease_lexicographic() {
let alpha = parse_semver("1.0.0-alpha").unwrap();
let beta = parse_semver("1.0.0-beta").unwrap();
assert!(alpha < beta);
}
#[test]
fn test_semver_to_string_repr() {
let v = parse_semver("1.2.3").unwrap();
assert_eq!(v.to_string_repr(), "1.2.3");
let v_pre = parse_semver("1.2.3-rc1").unwrap();
assert_eq!(v_pre.to_string_repr(), "1.2.3-rc1");
}
#[test]
fn test_negotiate_same_version() {
let result = negotiate_version("1.2.3", "1.2.3").unwrap();
assert_eq!(result, "1.2.3");
}
#[test]
fn test_negotiate_declared_older_minor() {
let result = negotiate_version("1.1.0", "1.3.0").unwrap();
assert_eq!(result, "1.1.0");
}
#[test]
fn test_negotiate_declared_newer_minor_fails() {
let result = negotiate_version("1.5.0", "1.3.0");
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, ErrorCode::VersionIncompatible);
}
#[test]
fn test_negotiate_major_mismatch_fails() {
let result = negotiate_version("2.0.0", "1.0.0");
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, ErrorCode::VersionIncompatible);
}
#[test]
fn test_negotiate_same_minor_higher_sdk_patch() {
let result = negotiate_version("1.2.0", "1.2.5").unwrap();
assert_eq!(result, "1.2.5");
}
#[test]
fn test_negotiate_same_minor_higher_declared_patch() {
let result = negotiate_version("1.2.5", "1.2.0").unwrap();
assert_eq!(result, "1.2.5");
}
#[test]
fn test_negotiate_prerelease_vs_release_same_version() {
let result = negotiate_version("1.2.0-alpha", "1.2.0").unwrap();
assert_eq!(result, "1.2.0");
}
#[test]
fn test_negotiate_invalid_declared_version() {
let result = negotiate_version("not.a.version", "1.0.0");
assert!(result.is_err());
}
#[test]
fn test_negotiate_invalid_sdk_version() {
let result = negotiate_version("1.0.0", "bad");
assert!(result.is_err());
}
}