use anyhow::bail;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginIdVersion {
V1,
Legacy,
}
pub fn validate_plugin_id_v1(plugin_id: &str) -> anyhow::Result<()> {
if plugin_id.len() > 253 {
bail!("Plugin id v1 must not exceed 253 bytes");
}
let labels = plugin_id.split('.').collect::<Vec<_>>();
if labels.len() < 2 || !labels.iter().all(valid_plugin_label) {
bail!(
"Plugin id v1 must contain at least two lowercase dot-separated labels; labels start with a letter, end with a letter or digit, and may contain hyphens"
);
}
Ok(())
}
pub fn classify_existing_plugin_id(plugin_id: &str) -> anyhow::Result<PluginIdVersion> {
if validate_plugin_id_v1(plugin_id).is_ok() {
return Ok(PluginIdVersion::V1);
}
if plugin_id.is_empty()
|| plugin_id.contains('.')
|| !plugin_id
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b".-".contains(&byte))
|| !plugin_id
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_lowercase())
{
bail!("Plugin id is neither a canonical v1 identity nor a supported legacy identity");
}
Ok(PluginIdVersion::Legacy)
}
pub fn validate_release_version(version: &str) -> anyhow::Result<()> {
semver::Version::parse(version)
.map(|_| ())
.map_err(|error| {
anyhow::anyhow!("Release version must be an exact Semantic Version: {error}")
})
}
fn valid_plugin_label(label: &&str) -> bool {
let bytes = label.as_bytes();
(1..=63).contains(&bytes.len())
&& bytes[0].is_ascii_lowercase()
&& bytes[bytes.len() - 1].is_ascii_alphanumeric()
&& bytes
.iter()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
}
#[cfg(test)]
mod tests {
use super::*;
fn vectors() -> serde_json::Value {
serde_json::from_str(include_str!(
"../contracts/plugin-identity-v1.conformance.json"
))
.unwrap()
}
#[test]
fn rust_validator_obeys_published_plugin_id_vectors() {
let vectors = vectors();
for value in vectors["pluginId"]["valid"].as_array().unwrap() {
let value = value.as_str().unwrap();
assert!(
validate_plugin_id_v1(value).is_ok(),
"expected valid: {value}"
);
}
for value in vectors["pluginId"]["invalid"].as_array().unwrap() {
let value = value.as_str().unwrap();
assert!(
validate_plugin_id_v1(value).is_err(),
"expected invalid: {value}"
);
}
}
#[test]
fn rust_validator_obeys_published_semver_vectors() {
let vectors = vectors();
for value in vectors["version"]["valid"].as_array().unwrap() {
let value = value.as_str().unwrap();
assert!(
validate_release_version(value).is_ok(),
"expected valid: {value}"
);
}
for value in vectors["version"]["invalid"].as_array().unwrap() {
let value = value.as_str().unwrap();
assert!(
validate_release_version(value).is_err(),
"expected invalid: {value}"
);
}
}
#[test]
fn existing_projects_can_be_opened_with_an_explicit_legacy_classification() {
assert_eq!(
classify_existing_plugin_id("uppercase").unwrap(),
PluginIdVersion::Legacy
);
assert_eq!(
classify_existing_plugin_id("company.uppercase").unwrap(),
PluginIdVersion::V1
);
assert_eq!(
classify_existing_plugin_id("uppercase-v2").unwrap(),
PluginIdVersion::Legacy
);
assert!(classify_existing_plugin_id("company..uppercase").is_err());
}
}