#[must_use]
pub fn parse_release_tag(json: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(json).ok()?;
let tag = parsed.get("tag_name")?.as_str()?;
let trimmed = tag.strip_prefix('v').unwrap_or(tag);
if trimmed.contains('-') {
return None;
}
if !is_stable_semver(trimmed) {
return None;
}
Some(trimmed.to_string())
}
#[must_use]
pub fn is_stable_semver(s: &str) -> bool {
let parts: Vec<&str> = s.split('.').collect();
parts.len() == 3
&& parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}
#[must_use]
pub fn compare_stable_versions(a: &str, b: &str) -> std::cmp::Ordering {
let parse = |s: &str| -> [u64; 3] {
let mut parts = s.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
[
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
]
};
parse(a).cmp(&parse(b))
}