use std::path::Path;
use crate::contract::Scope;
pub fn validate_version(version: &str) -> bool {
if version.is_empty() {
return false;
}
let ver = if let Some(pos) = version.find('/') {
let scope = &version[..pos];
if scope.is_empty()
|| !scope
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '-')
{
return false;
}
&version[pos + 1..]
} else {
version
};
let without_v = match ver.strip_prefix('v') {
Some(v) => v,
None => return false,
};
let (semver, _prerelease) = if let Some(dash) = without_v.find('-') {
let sv = &without_v[..dash];
let pr = &without_v[dash + 1..];
if pr.is_empty() || pr.starts_with('.') {
return false;
}
(sv, Some(pr))
} else {
(without_v, None)
};
let parts: Vec<&str> = semver.split('.').collect();
if parts.len() != 3 {
return false;
}
parts
.iter()
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
}
pub fn normalize_version(version: &str) -> String {
let after_scope = version.split('/').next_back().unwrap_or(version);
after_scope
.strip_prefix('v')
.unwrap_or(after_scope)
.to_string()
}
#[derive(Debug)]
pub struct VersionState {
pub tag_version: Option<String>,
pub config_version: Option<String>,
pub consistent: bool,
pub config_files: Vec<(String, Option<String>)>,
}
pub fn check_version_consistency(
tag_version: Option<&str>,
config_files: &[(String, Option<String>)],
) -> bool {
match tag_version {
Some(t) => config_files.iter().all(|(_, v)| match v {
Some(cv) => cv == t,
None => true,
}),
None => config_files.iter().all(|(_, v)| v.is_none()),
}
}
pub fn verify_version(
repo_path: &Path,
scope: &Scope,
) -> Result<VersionState, Box<dyn std::error::Error>> {
let tag_version = crate::source::git_tag::latest_tag(repo_path, &scope.name)?;
let scope_dir = repo_path.join(&scope.dir);
let config_files = crate::source::config_file::read_config_versions(&scope_dir);
let config_version = config_files
.iter()
.find(|(_, v)| v.is_some())
.and_then(|(_, v)| v.clone());
let consistent = check_version_consistency(tag_version.as_deref(), &config_files);
Ok(VersionState {
tag_version,
config_version,
consistent,
config_files,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_version_standard() {
assert!(validate_version("v1.2.3"));
}
#[test]
fn test_validate_version_prerelease() {
assert!(validate_version("v1.2.3-rc.1"));
assert!(validate_version("v1.2.3-alpha"));
}
#[test]
fn test_validate_version_scoped() {
assert!(validate_version("cli/v1.2.3"));
assert!(validate_version("pkg.name/v0.1.0"));
}
#[test]
fn test_validate_version_no_v() {
assert!(!validate_version("1.2.3"));
}
#[test]
fn test_validate_version_incomplete() {
assert!(!validate_version("v1.2"));
assert!(!validate_version("v1"));
}
#[test]
fn test_validate_version_empty() {
assert!(!validate_version(""));
}
#[test]
fn test_validate_version_scope_only() {
assert!(!validate_version("cli/"));
}
#[test]
fn test_normalize_version_v_prefix() {
assert_eq!(normalize_version("v1.2.3"), "1.2.3");
}
#[test]
fn test_normalize_version_scoped() {
assert_eq!(normalize_version("cli/v0.5.0"), "0.5.0");
}
#[test]
fn test_normalize_version_no_prefix() {
assert_eq!(normalize_version("1.2.3"), "1.2.3");
}
#[test]
fn test_consistency_matches() {
assert!(check_version_consistency(
Some("0.1.0"),
&[("Cargo.toml".into(), Some("0.1.0".into()))]
));
}
#[test]
fn test_consistency_mismatch() {
assert!(!check_version_consistency(
Some("0.1.0"),
&[("Cargo.toml".into(), Some("0.2.0".into()))]
));
}
#[test]
fn test_consistency_config_no_version() {
assert!(check_version_consistency(
Some("0.1.0"),
&[("Cargo.toml".into(), None)]
));
}
#[test]
fn test_consistency_no_tag_no_config() {
assert!(check_version_consistency(
None,
&[("Cargo.toml".into(), None)]
));
}
#[test]
fn test_consistency_no_tag_but_config_has_version() {
assert!(!check_version_consistency(
None,
&[("Cargo.toml".into(), Some("0.1.0".into()))]
));
}
#[test]
fn test_consistency_multi_file_all_match() {
let files = vec![
("Cargo.toml".into(), Some("0.1.0".into())),
("pyproject.toml".into(), Some("0.1.0".into())),
];
assert!(check_version_consistency(Some("0.1.0"), &files));
}
#[test]
fn test_consistency_multi_file_one_mismatch() {
let files = vec![
("Cargo.toml".into(), Some("0.1.0".into())),
("pyproject.toml".into(), Some("0.2.0".into())),
];
assert!(!check_version_consistency(Some("0.1.0"), &files));
}
}