#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateError {
UnknownTemplate(String),
MissingField {
template: String,
field: String,
},
InvalidValue {
field: String,
reason: String,
},
}
impl std::fmt::Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
TemplateError::UnknownTemplate(t) => write!(f, "Unknown template type: {}", t),
TemplateError::MissingField { template, field } => {
write!(f, "{} template requires '{}' field", template, field)
}
TemplateError::InvalidValue { field, reason } => {
write!(f, "Invalid value for '{}': {}", field, reason)
}
}
}
}
impl std::error::Error for TemplateError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Template {
GitHub {
owner: String,
repository: String,
release_only: bool,
version_type: Option<String>,
},
GitLab {
dist: String,
release_only: bool,
version_type: Option<String>,
},
PyPI {
package: String,
version_type: Option<String>,
},
Npmregistry {
package: String,
version_type: Option<String>,
},
Metacpan {
dist: String,
version_type: Option<String>,
},
}
#[derive(Debug, Clone, Default)]
pub struct ExpandedTemplate {
pub source: Option<String>,
pub matching_pattern: Option<String>,
pub searchmode: Option<String>,
pub mode: Option<String>,
pub pgpmode: Option<String>,
pub downloadurlmangle: Option<String>,
}
pub fn expand_template(template: Template) -> ExpandedTemplate {
match template {
Template::GitHub {
owner,
repository,
release_only,
version_type,
} => expand_github_template(owner, repository, release_only, version_type),
Template::GitLab {
dist,
release_only,
version_type,
} => expand_gitlab_template(dist, release_only, version_type),
Template::PyPI {
package,
version_type,
} => expand_pypi_template(package, version_type),
Template::Npmregistry {
package,
version_type,
} => expand_npmregistry_template(package, version_type),
Template::Metacpan { dist, version_type } => expand_metacpan_template(dist, version_type),
}
}
fn expand_github_template(
owner: String,
repository: String,
release_only: bool,
version_type: Option<String>,
) -> ExpandedTemplate {
let version_pattern = version_type
.as_deref()
.map(|v| format!("@{}_VERSION@", v.to_uppercase()))
.unwrap_or_else(|| "@ANY_VERSION@".to_string());
let source = if release_only {
format!("https://github.com/{}/{}/releases", owner, repository)
} else {
format!("https://github.com/{}/{}/tags", owner, repository)
};
let matching_pattern = format!(
r".*/(?:refs/tags/)?v?{}{}",
version_pattern, "@ARCHIVE_EXT@"
);
ExpandedTemplate {
source: Some(source),
matching_pattern: Some(matching_pattern),
searchmode: Some("html".to_string()),
..Default::default()
}
}
fn expand_gitlab_template(
dist: String,
_release_only: bool,
version_type: Option<String>,
) -> ExpandedTemplate {
let version_pattern = version_type
.as_deref()
.map(|v| format!("@{}_VERSION@", v.to_uppercase()))
.unwrap_or_else(|| "@ANY_VERSION@".to_string());
ExpandedTemplate {
source: Some(dist),
matching_pattern: Some(format!(r".*/v?{}{}", version_pattern, "@ARCHIVE_EXT@")),
mode: Some("gitlab".to_string()),
..Default::default()
}
}
fn expand_pypi_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
let version_pattern = version_type
.as_deref()
.map(|v| format!("@{}_VERSION@", v.to_uppercase()))
.unwrap_or_else(|| "@ANY_VERSION@".to_string());
ExpandedTemplate {
source: Some(format!("https://pypi.debian.net/{}/", package)),
matching_pattern: Some(format!(
r"https://pypi\.debian\.net/{}/[^/]+\.tar\.gz#/.*-{}\.tar\.gz",
package, version_pattern
)),
searchmode: Some("plain".to_string()),
..Default::default()
}
}
fn expand_npmregistry_template(package: String, version_type: Option<String>) -> ExpandedTemplate {
let version_pattern = version_type
.as_deref()
.map(|v| format!("@{}_VERSION@", v.to_uppercase()))
.unwrap_or_else(|| "@ANY_VERSION@".to_string());
let package_name = package.trim_start_matches('@');
ExpandedTemplate {
source: Some(format!("https://registry.npmjs.org/{}", package)),
matching_pattern: Some(format!(
r"https://registry\.npmjs\.org/{}/-/.*-{}@ARCHIVE_EXT@",
package_name.replace('/', r"\/"),
version_pattern
)),
searchmode: Some("plain".to_string()),
..Default::default()
}
}
fn expand_metacpan_template(dist: String, version_type: Option<String>) -> ExpandedTemplate {
let version_pattern = version_type
.as_deref()
.map(|v| format!("@{}_VERSION@", v.to_uppercase()))
.unwrap_or_else(|| "@ANY_VERSION@".to_string());
let dist_name = dist.replace("::", "-");
ExpandedTemplate {
source: Some("https://cpan.metacpan.org/authors/id/".to_string()),
matching_pattern: Some(format!(r".*/{}{}@ARCHIVE_EXT@", dist_name, version_pattern)),
searchmode: Some("plain".to_string()),
..Default::default()
}
}
pub fn detect_template(
source: Option<&str>,
matching_pattern: Option<&str>,
searchmode: Option<&str>,
mode: Option<&str>,
) -> Option<Template> {
let source = source?;
if let Some(template) = detect_github_template(source, matching_pattern, searchmode) {
return Some(template);
}
if let Some(template) = detect_gitlab_template(source, matching_pattern, mode) {
return Some(template);
}
if let Some(template) = detect_pypi_template(source, matching_pattern, searchmode) {
return Some(template);
}
if let Some(template) = detect_npmregistry_template(source, matching_pattern, searchmode) {
return Some(template);
}
if let Some(template) = detect_metacpan_template(source, matching_pattern, searchmode) {
return Some(template);
}
None
}
fn detect_github_template(
source: &str,
matching_pattern: Option<&str>,
searchmode: Option<&str>,
) -> Option<Template> {
if searchmode != Some("html") && searchmode.is_some() {
return None;
}
let release_only = if source.ends_with("/releases") {
true
} else if source.ends_with("/tags") {
false
} else {
return None;
};
let url_without_suffix = if release_only {
source.strip_suffix("/releases")?
} else {
source.strip_suffix("/tags")?
};
let (owner, repository) = if let Ok(parsed) = url::Url::parse(url_without_suffix) {
if parsed.host_str() != Some("github.com") {
return None;
}
let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
let parts: Vec<&str> = path.split('/').collect();
if parts.len() != 2 {
return None;
}
(parts[0].to_string(), parts[1].to_string())
} else {
return None;
};
let version_type = if let Some(pattern) = matching_pattern {
extract_version_type(pattern)
} else {
None
};
Some(Template::GitHub {
owner,
repository,
release_only,
version_type,
})
}
fn detect_gitlab_template(
source: &str,
matching_pattern: Option<&str>,
mode: Option<&str>,
) -> Option<Template> {
if mode != Some("gitlab") {
return None;
}
let version_type = if let Some(pattern) = matching_pattern {
extract_version_type(pattern)
} else {
None
};
Some(Template::GitLab {
dist: source.to_string(),
release_only: false, version_type,
})
}
fn detect_pypi_template(
source: &str,
matching_pattern: Option<&str>,
searchmode: Option<&str>,
) -> Option<Template> {
if searchmode != Some("plain") && searchmode.is_some() {
return None;
}
if !source.starts_with("https://pypi.debian.net/") {
return None;
}
let package = source
.strip_prefix("https://pypi.debian.net/")?
.trim_end_matches('/');
let version_type = if let Some(pattern) = matching_pattern {
extract_version_type(pattern)
} else {
None
};
Some(Template::PyPI {
package: package.to_string(),
version_type,
})
}
fn detect_npmregistry_template(
source: &str,
matching_pattern: Option<&str>,
searchmode: Option<&str>,
) -> Option<Template> {
if searchmode != Some("plain") && searchmode.is_some() {
return None;
}
if !source.starts_with("https://registry.npmjs.org/") {
return None;
}
let package = source.strip_prefix("https://registry.npmjs.org/")?;
let version_type = if let Some(pattern) = matching_pattern {
extract_version_type(pattern)
} else {
None
};
Some(Template::Npmregistry {
package: package.to_string(),
version_type,
})
}
fn detect_metacpan_template(
source: &str,
matching_pattern: Option<&str>,
searchmode: Option<&str>,
) -> Option<Template> {
if searchmode != Some("plain") && searchmode.is_some() {
return None;
}
if source == "https://cpan.metacpan.org/authors/id/" {
let pattern = matching_pattern?;
if !pattern.starts_with(".*/") {
return None;
}
let after_prefix = pattern.strip_prefix(".*/").unwrap();
let version_type = extract_version_type(pattern);
let dist = if let Some(idx) = after_prefix.find('@') {
after_prefix[..idx].trim_end_matches("-v?").trim_end_matches('-')
} else {
return None;
};
Some(Template::Metacpan {
dist: dist.to_string(),
version_type,
})
} else if let Some(dist) = source
.strip_prefix("https://metacpan.org/release/")
.or_else(|| source.strip_prefix("https://metacpan.org/dist/"))
{
let dist = dist.trim_end_matches('/');
if dist.is_empty() {
return None;
}
let version_type = matching_pattern.and_then(extract_version_type);
Some(Template::Metacpan {
dist: dist.to_string(),
version_type,
})
} else {
None
}
}
fn extract_version_type(pattern: &str) -> Option<String> {
if pattern.contains("@ANY_VERSION@") {
None
} else if let Some(start) = pattern.find('@') {
if let Some(end) = pattern[start + 1..].find('@') {
let version_str = &pattern[start + 1..start + 1 + end];
if version_str.ends_with("_VERSION") {
let type_str = version_str.strip_suffix("_VERSION")?;
Some(type_str.to_lowercase())
} else {
None
}
} else {
None
}
} else {
None
}
}
pub fn parse_github_url(url: &str) -> Result<(String, String), TemplateError> {
let url = url.trim_end_matches('/');
if let Ok(parsed) = url::Url::parse(url) {
if parsed.host_str() == Some("github.com") {
let path = parsed.path().trim_start_matches('/').trim_end_matches('/');
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 2 {
return Ok((parts[0].to_string(), parts[1].to_string()));
}
}
}
let parts: Vec<&str> = url.split('/').collect();
if parts.len() == 2 {
return Ok((parts[0].to_string(), parts[1].to_string()));
}
Err(TemplateError::InvalidValue {
field: "Dist".to_string(),
reason: format!("Could not parse GitHub URL: {}", url),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_github_template_with_owner_repository() {
let template = Template::GitHub {
owner: "torvalds".to_string(),
repository: "linux".to_string(),
release_only: false,
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://github.com/torvalds/linux/tags".to_string())
);
assert_eq!(
result.matching_pattern,
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@".to_string())
);
}
#[test]
fn test_github_template_release_only() {
let template = Template::GitHub {
owner: "test".to_string(),
repository: "project".to_string(),
release_only: true,
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://github.com/test/project/releases".to_string())
);
}
#[test]
fn test_parse_github_url() {
let (owner, repo) = parse_github_url("https://github.com/guimard/llng-docker").unwrap();
assert_eq!(owner, "guimard");
assert_eq!(repo, "llng-docker");
let (owner, repo) = parse_github_url("torvalds/linux").unwrap();
assert_eq!(owner, "torvalds");
assert_eq!(repo, "linux");
}
#[test]
fn test_pypi_template() {
let template = Template::PyPI {
package: "bitbox02".to_string(),
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://pypi.debian.net/bitbox02/".to_string())
);
assert_eq!(result.searchmode, Some("plain".to_string()));
}
#[test]
fn test_npmregistry_template() {
let template = Template::Npmregistry {
package: "@lemonldapng/handler".to_string(),
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://registry.npmjs.org/@lemonldapng/handler".to_string())
);
assert_eq!(result.searchmode, Some("plain".to_string()));
}
#[test]
fn test_gitlab_template() {
let template = Template::GitLab {
dist: "https://salsa.debian.org/debian/devscripts".to_string(),
release_only: false,
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://salsa.debian.org/debian/devscripts".to_string())
);
assert_eq!(result.mode, Some("gitlab".to_string()));
}
#[test]
fn test_metacpan_template() {
let template = Template::Metacpan {
dist: "MetaCPAN-Client".to_string(),
version_type: None,
};
let result = expand_template(template);
assert_eq!(
result.source,
Some("https://cpan.metacpan.org/authors/id/".to_string())
);
}
#[test]
fn test_detect_github_template() {
let template = detect_template(
Some("https://github.com/torvalds/linux/tags"),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(
template,
Some(Template::GitHub {
owner: "torvalds".to_string(),
repository: "linux".to_string(),
release_only: false,
version_type: None,
})
);
}
#[test]
fn test_detect_github_template_releases() {
let template = detect_template(
Some("https://github.com/test/project/releases"),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(
template,
Some(Template::GitHub {
owner: "test".to_string(),
repository: "project".to_string(),
release_only: true,
version_type: None,
})
);
}
#[test]
fn test_detect_github_template_with_version_type() {
let template = detect_template(
Some("https://github.com/foo/bar/tags"),
Some(r".*/(?:refs/tags/)?v?@SEMANTIC_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(
template,
Some(Template::GitHub {
owner: "foo".to_string(),
repository: "bar".to_string(),
release_only: false,
version_type: Some("semantic".to_string()),
})
);
}
#[test]
fn test_detect_pypi_template() {
let template = detect_template(
Some("https://pypi.debian.net/bitbox02/"),
Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
Some("plain"),
None,
);
assert_eq!(
template,
Some(Template::PyPI {
package: "bitbox02".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_gitlab_template() {
let template = detect_template(
Some("https://salsa.debian.org/debian/devscripts"),
Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
None,
Some("gitlab"),
);
assert_eq!(
template,
Some(Template::GitLab {
dist: "https://salsa.debian.org/debian/devscripts".to_string(),
release_only: false,
version_type: None,
})
);
}
#[test]
fn test_detect_npmregistry_template() {
let template = detect_template(
Some("https://registry.npmjs.org/@lemonldapng/handler"),
Some(
r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
),
Some("plain"),
None,
);
assert_eq!(
template,
Some(Template::Npmregistry {
package: "@lemonldapng/handler".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_metacpan_template() {
let template = detect_template(
Some("https://cpan.metacpan.org/authors/id/"),
Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
Some("plain"),
None,
);
assert_eq!(
template,
Some(Template::Metacpan {
dist: "MetaCPAN-Client".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_no_template() {
let template = detect_template(
Some("https://example.com/downloads/"),
Some(r".*/v?(\d+\.\d+)\.tar\.gz"),
Some("html"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_roundtrip_github_template() {
let original = Template::GitHub {
owner: "torvalds".to_string(),
repository: "linux".to_string(),
release_only: false,
version_type: None,
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_extract_version_type() {
assert_eq!(extract_version_type("@ANY_VERSION@"), None);
assert_eq!(
extract_version_type("@SEMANTIC_VERSION@"),
Some("semantic".to_string())
);
assert_eq!(
extract_version_type("@STABLE_VERSION@"),
Some("stable".to_string())
);
assert_eq!(extract_version_type("no-template-here"), None);
}
#[test]
fn test_detect_github_wrong_searchmode() {
let template = detect_template(
Some("https://github.com/torvalds/linux/tags"),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("plain"), None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_github_invalid_url() {
let template = detect_template(
Some("https://github.com/torvalds/linux"),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_github_wrong_host() {
let template = detect_template(
Some("https://gitlab.com/foo/bar/tags"),
Some(r".*/(?:refs/tags/)?v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_gitlab_without_mode() {
let template = detect_template(
Some("https://salsa.debian.org/debian/devscripts"),
Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
None,
None, );
assert_eq!(template, None);
}
#[test]
fn test_detect_pypi_wrong_searchmode() {
let template = detect_template(
Some("https://pypi.debian.net/bitbox02/"),
Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
Some("html"), None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_pypi_wrong_url() {
let template = detect_template(
Some("https://pypi.org/bitbox02/"), Some(r"https://pypi\.debian\.net/bitbox02/[^/]+\.tar\.gz#/.*-@ANY_VERSION@\.tar\.gz"),
Some("plain"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_npmregistry_wrong_url() {
let template = detect_template(
Some("https://npm.example.com/@lemonldapng/handler"), Some(
r"https://registry\.npmjs\.org/lemonldapng/handler/-/.*-@ANY_VERSION@@ARCHIVE_EXT@",
),
Some("plain"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_metacpan_wrong_source() {
let template = detect_template(
Some("https://cpan.example.com/authors/id/"), Some(r".*/MetaCPAN-Client@ANY_VERSION@@ARCHIVE_EXT@"),
Some("plain"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_metacpan_missing_pattern() {
let template = detect_template(
Some("https://cpan.metacpan.org/authors/id/"),
None, Some("plain"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_metacpan_release_url() {
let template = detect_template(
Some("https://metacpan.org/release/Time-ParseDate"),
Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
None,
None,
);
assert_eq!(
template,
Some(Template::Metacpan {
dist: "Time-ParseDate".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_metacpan_dist_url() {
let template = detect_template(
Some("https://metacpan.org/dist/Mail-AuthenticationResults"),
Some(r".*/Mail-AuthenticationResults-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
None,
None,
);
assert_eq!(
template,
Some(Template::Metacpan {
dist: "Mail-AuthenticationResults".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_metacpan_cpan_url_with_v_prefix() {
let template = detect_template(
Some("https://cpan.metacpan.org/authors/id/"),
Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("plain"),
None,
);
assert_eq!(
template,
Some(Template::Metacpan {
dist: "Time-ParseDate".to_string(),
version_type: None,
})
);
}
#[test]
fn test_detect_metacpan_release_url_wrong_domain() {
let template = detect_template(
Some("https://example.org/release/Time-ParseDate"),
Some(r".*/Time-ParseDate-v?@ANY_VERSION@@ARCHIVE_EXT@$"),
None,
None,
);
assert_eq!(template, None);
}
#[test]
fn test_roundtrip_gitlab_template() {
let original = Template::GitLab {
dist: "https://salsa.debian.org/debian/devscripts".to_string(),
release_only: false,
version_type: None,
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_roundtrip_pypi_template() {
let original = Template::PyPI {
package: "bitbox02".to_string(),
version_type: None,
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_roundtrip_npmregistry_template() {
let original = Template::Npmregistry {
package: "@scope/package".to_string(),
version_type: None,
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_roundtrip_metacpan_template() {
let original = Template::Metacpan {
dist: "MetaCPAN-Client".to_string(),
version_type: None,
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_roundtrip_github_with_version_type() {
let original = Template::GitHub {
owner: "foo".to_string(),
repository: "bar".to_string(),
release_only: true,
version_type: Some("stable".to_string()),
};
let expanded = expand_template(original.clone());
let detected = detect_template(
expanded.source.as_deref(),
expanded.matching_pattern.as_deref(),
expanded.searchmode.as_deref(),
expanded.mode.as_deref(),
);
assert_eq!(detected, Some(original));
}
#[test]
fn test_detect_with_none_source() {
let template = detect_template(
None,
Some(r".*/v?@ANY_VERSION@@ARCHIVE_EXT@"),
Some("html"),
None,
);
assert_eq!(template, None);
}
#[test]
fn test_detect_github_partial_match() {
let template = detect_template(
Some("https://github.com/torvalds/linux/tags"),
Some(r".*/v?(\d+\.\d+)\.tar\.gz"), Some("html"),
None,
);
assert_eq!(
template,
Some(Template::GitHub {
owner: "torvalds".to_string(),
repository: "linux".to_string(),
release_only: false,
version_type: None,
})
);
}
#[test]
fn test_extract_version_type_edge_cases() {
assert_eq!(extract_version_type("@FOO@@BAR@"), None);
assert_eq!(extract_version_type("@INCOMPLETE"), None);
assert_eq!(extract_version_type("@SOMETHING@"), None);
assert_eq!(extract_version_type("@@"), None);
}
}