use crate::error::BioLibError;
#[derive(Debug, Clone)]
pub struct SemanticVersion {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
impl std::fmt::Display for SemanticVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone)]
pub struct ResourceUri {
pub resource_prefix: Option<String>,
pub account_handle: String,
pub account_handle_normalized: String,
pub resource_name: Option<String>,
pub resource_name_normalized: Option<String>,
pub version: Option<SemanticVersion>,
pub tag: Option<String>,
}
fn normalize_resource_name(s: &str) -> String {
s.replace('-', "_").to_lowercase()
}
fn is_handle_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn is_prefix_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '.' || c == '-'
}
fn is_valid_tag(s: &str) -> bool {
!s.is_empty()
&& s.len() <= 128
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
pub fn parse_semantic_version(version_str: &str) -> std::result::Result<SemanticVersion, String> {
let parts: Vec<&str> = version_str.split('.').collect();
if parts.len() != 3 {
return Err("The version must be a valid semantic version in the format of major.minor.patch (1.2.3).".to_string());
}
let parse_part = |s: &str| -> std::result::Result<u64, String> {
if s.is_empty() || (s.len() > 1 && s.starts_with('0')) {
return Err("The version must be a valid semantic version in the format of major.minor.patch (1.2.3).".to_string());
}
s.parse::<u64>().map_err(|_| {
"The version must be a valid semantic version in the format of major.minor.patch (1.2.3).".to_string()
})
};
Ok(SemanticVersion {
major: parse_part(parts[0])?,
minor: parse_part(parts[1])?,
patch: parse_part(parts[2])?,
})
}
pub fn parse_resource_uri(uri: &str) -> crate::Result<ResourceUri> {
let remaining = uri;
let (resource_prefix, remaining) = if let Some(stripped) = remaining.strip_prefix('@') {
match stripped.find('/') {
Some(slash_pos) => {
let prefix = &stripped[..slash_pos];
if prefix.is_empty() || !prefix.chars().all(is_prefix_char) {
return Err(BioLibError::Validation(format!(
"Could not parse resource uri '{uri}'"
)));
}
(Some(prefix.to_lowercase()), &stripped[slash_pos + 1..])
}
None => {
return Err(BioLibError::Validation(format!(
"Could not parse resource uri '{uri}'"
)))
}
}
} else {
(None, remaining)
};
let (main_part, suffix) = match remaining.find(':') {
Some(colon_pos) => (&remaining[..colon_pos], Some(&remaining[colon_pos + 1..])),
None => (remaining, None),
};
let (account_handle, resource_name) = match main_part.find('/') {
Some(slash_pos) => {
let handle = &main_part[..slash_pos];
let name = &main_part[slash_pos + 1..];
if name.contains('/') {
return Err(BioLibError::Validation(format!(
"Could not parse resource uri '{uri}'"
)));
}
(handle, if name.is_empty() { None } else { Some(name) })
}
None => (main_part, None),
};
if account_handle.is_empty() || !account_handle.chars().all(is_handle_char) {
return Err(BioLibError::Validation(format!(
"Could not parse resource uri '{uri}'"
)));
}
if let Some(name) = resource_name {
if !name.chars().all(is_handle_char) {
return Err(BioLibError::Validation(format!(
"Could not parse resource uri '{uri}'"
)));
}
}
let mut version = None;
let mut tag = None;
if let Some(suffix) = suffix {
if suffix != "*" {
match parse_semantic_version(suffix) {
Ok(v) => version = Some(v),
Err(_) => {
if is_valid_tag(suffix) {
tag = Some(suffix.to_string());
} else {
return Err(BioLibError::Validation(format!(
"Invalid version or tag \"{suffix}\". \
Versions must be semantic versions like \"1.2.3\". \
Tags must be lowercase alphanumeric or dashes and at most 128 characters."
)));
}
}
}
}
}
let account_handle = account_handle.to_string();
let account_handle_normalized = normalize_resource_name(&account_handle);
let resource_name = resource_name.map(|n| n.to_string());
let resource_name_normalized = if let Some(ref name) = resource_name {
Some(normalize_resource_name(name))
} else {
Some(account_handle_normalized.clone())
};
Ok(ResourceUri {
resource_prefix,
account_handle,
account_handle_normalized,
resource_name_normalized,
resource_name,
version,
tag,
})
}