biolib 1.3.279

BioLib client library and CLI for running applications on BioLib
Documentation
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])?,
    })
}

// Parses URIs matching: (@<prefix>/)?<account_handle>(/<resource_name>)?(:<suffix>)?
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,
    })
}