kcode-k1-web-package 0.1.0

Validate authority-namespaced buildless K1 Web package source
Documentation
use kcode_k1_transaction_id::TxId;
use semver::Version;
use std::fmt::{Display, Formatter};

mod source;
pub use source::{SourceFile, SourcePackage, WebDependency};

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AuthorityId(TxId);

impl AuthorityId {
    pub const fn new(transaction_id: TxId) -> Self {
        Self(transaction_id)
    }

    pub const fn transaction_id(&self) -> &TxId {
        &self.0
    }
}

impl Display for AuthorityId {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.0, formatter)
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WebFamily(AuthorityId, String);

impl WebFamily {
    pub fn new(
        authority: AuthorityId,
        logical_name: impl Into<String>,
    ) -> Result<Self, PackageError> {
        let logical_name = logical_name.into();
        validate_logical_name(&logical_name)?;
        Ok(Self(authority, logical_name))
    }

    pub const fn authority(&self) -> AuthorityId {
        self.0
    }

    pub fn logical_name(&self) -> &str {
        &self.1
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WebId(WebFamily, Version);

impl WebId {
    pub fn new(family: WebFamily, version: Version) -> Result<Self, PackageError> {
        validate_stable(&version)?;
        Ok(Self(family, version))
    }

    pub fn family(&self) -> &WebFamily {
        &self.0
    }

    pub fn version(&self) -> &Version {
        &self.1
    }
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
enum SelectorKind {
    Any,
    Major(u64),
    MajorMinor(u64, u64),
    Exact(u64, u64, u64),
}

#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DependencySelector(SelectorKind);

impl DependencySelector {
    pub fn parse(text: &str) -> Result<Self, PackageError> {
        if text == "*" {
            return Ok(Self(SelectorKind::Any));
        }
        let numbers = text
            .split('.')
            .map(parse_selector_number)
            .collect::<Result<Vec<_>, _>>()?;
        let kind = match numbers.as_slice() {
            [major] => SelectorKind::Major(*major),
            [major, minor] => SelectorKind::MajorMinor(*major, *minor),
            [major, minor, patch] => SelectorKind::Exact(*major, *minor, *patch),
            _ => return fail("dependency selector must contain one to three numbers or *"),
        };
        Ok(Self(kind))
    }

    pub fn matches(&self, version: &Version) -> bool {
        if !version.pre.is_empty() || !version.build.is_empty() {
            return false;
        }
        match self.0 {
            SelectorKind::Any => true,
            SelectorKind::Major(major) => version.major == major,
            SelectorKind::MajorMinor(major, minor) => {
                version.major == major && version.minor == minor
            }
            SelectorKind::Exact(major, minor, patch) => {
                version.major == major && version.minor == minor && version.patch == patch
            }
        }
    }
}

impl Display for DependencySelector {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        match self.0 {
            SelectorKind::Any => formatter.write_str("*"),
            SelectorKind::Major(major) => write!(formatter, "{major}"),
            SelectorKind::MajorMinor(major, minor) => write!(formatter, "{major}.{minor}"),
            SelectorKind::Exact(major, minor, patch) => {
                write!(formatter, "{major}.{minor}.{patch}")
            }
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackageError(String);

impl PackageError {
    pub fn message(&self) -> &str {
        &self.0
    }
}

impl Display for PackageError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.0)
    }
}

impl std::error::Error for PackageError {}

pub(crate) fn fail<T>(message: impl Into<String>) -> Result<T, PackageError> {
    Err(PackageError(message.into()))
}

pub(crate) fn validate_logical_name(name: &str) -> Result<(), PackageError> {
    let valid = !name.is_empty()
        && name.len() <= 36
        && name.split('-').all(|part| {
            !part.is_empty()
                && part
                    .bytes()
                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
        });
    if !valid {
        return fail("logical name must be 1-36 bytes of lowercase kebab case");
    }
    Ok(())
}

pub(crate) fn validate_stable(version: &Version) -> Result<(), PackageError> {
    if !version.pre.is_empty() || !version.build.is_empty() {
        return fail("version must be stable SemVer without prerelease or build metadata");
    }
    Ok(())
}

fn parse_selector_number(text: &str) -> Result<u64, PackageError> {
    let canonical = !text.is_empty()
        && !(text.len() > 1 && text.starts_with('0'))
        && text.bytes().all(|byte| byte.is_ascii_digit());
    if !canonical {
        return fail("dependency selector numbers must use canonical decimal spelling");
    }
    text.parse()
        .map_err(|_| PackageError("dependency selector number is too large".into()))
}