use crate::schema::FileKind;
pub fn cast_version(version: &str, target_kind: FileKind) -> Result<String, String> {
match target_kind {
FileKind::Any => Ok(version.to_string()),
FileKind::Simple => cast_to_simple(version),
FileKind::Python => cast_to_python(version),
FileKind::Semver => cast_to_semver(version),
}
}
fn cast_to_simple(version: &str) -> Result<String, String> {
let version = version.to_lowercase();
let version = if let Some(pos) = version.find('!') {
&version[pos + 1..]
} else {
version.as_str()
};
let version = if let Some(pos) = version.find('+') {
&version[..pos]
} else {
version
};
let release_end = find_release_end(version);
let release = &version[..release_end];
let parts: Vec<&str> = release.split('.').collect();
if parts.is_empty() {
return Err(format!("Cannot cast '{version}' to simple version: no version parts found"));
}
for part in &parts {
if part.parse::<u32>().is_err() {
return Err(format!("Cannot cast '{version}' to simple version: invalid part '{part}'"));
}
}
let major = parts.first().unwrap_or(&"0");
let minor = parts.get(1).unwrap_or(&"0");
let patch = parts.get(2).unwrap_or(&"0");
Ok(format!("{major}.{minor}.{patch}"))
}
fn cast_to_python(version: &str) -> Result<String, String> {
let parts: Vec<&str> = version.split('.').collect();
if parts.iter().all(|p| p.parse::<u32>().is_ok()) {
return Ok(version.to_string());
}
Ok(version.to_string())
}
fn cast_to_semver(version: &str) -> Result<String, String> {
let version = version.to_lowercase();
let version = if let Some(pos) = version.find('!') {
&version[pos + 1..]
} else {
version.as_str()
};
let version = if let Some(pos) = version.find('+') {
&version[..pos]
} else {
version
};
let release_end = find_release_end(version);
let release = &version[..release_end];
let suffix = &version[release_end..];
let parts: Vec<&str> = release.split('.').collect();
if parts.is_empty() {
return Err(format!("Cannot cast '{version}' to semver: no version parts found"));
}
for part in &parts {
if part.parse::<u32>().is_err() {
return Err(format!("Cannot cast '{version}' to semver: invalid part '{part}'"));
}
}
let major = parts.first().unwrap_or(&"0");
let minor = parts.get(1).unwrap_or(&"0");
let patch = parts.get(2).unwrap_or(&"0");
let base = format!("{major}.{minor}.{patch}");
if suffix.is_empty() {
return Ok(base);
}
let suffix = suffix
.split(".post")
.next()
.unwrap_or(suffix)
.split(".dev")
.next()
.unwrap_or(suffix);
if suffix.is_empty() {
return Ok(base);
}
let js_prerelease = if let Some(rest) = suffix.strip_prefix("alpha") {
format!("-alpha.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix('a') {
format!("-alpha.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix("beta") {
format!("-beta.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix('b') {
format!("-beta.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix("rc") {
format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix('c') {
format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else if let Some(rest) = suffix.strip_prefix("preview") {
format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
} else {
return Ok(base);
};
Ok(format!("{base}{js_prerelease}"))
}
fn find_release_end(version: &str) -> usize {
let markers = ["a", "b", "c", "alpha", "beta", "preview", "rc", ".post", ".dev", "-"];
let mut earliest = version.len();
for marker in markers {
if let Some(pos) = version.find(marker) {
let before = &version[..pos];
if before.is_empty() || before.ends_with('.') || before.chars().last().unwrap().is_ascii_digit() {
earliest = earliest.min(pos);
}
}
}
earliest
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cast_to_simple() {
assert_eq!(cast_to_simple("1.2.3").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1").unwrap(), "1.0.0");
assert_eq!(cast_to_simple("1.2").unwrap(), "1.2.0");
assert_eq!(cast_to_simple("1.2.3a1").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3b2").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3rc1").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3alpha1").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3beta2").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3.post1").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3.dev1").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1.2.3+local").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1!1.2.3").unwrap(), "1.2.3");
assert_eq!(cast_to_simple("1!1.2.3a1.post1.dev1+local").unwrap(), "1.2.3");
}
#[test]
fn test_cast_to_python() {
assert_eq!(cast_to_python("1.2.3").unwrap(), "1.2.3");
assert_eq!(cast_to_python("1.2.3a1").unwrap(), "1.2.3a1");
}
}