relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
use std::{cmp::Ordering, fmt};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct StableVersion {
    major: u64,
    minor: u64,
    patch: u64,
    prerelease: bool,
}

impl StableVersion {
    pub(super) const fn new(major: u64, minor: u64, patch: u64) -> Self {
        Self::from_parts(major, minor, patch, false)
    }

    pub(super) const fn prerelease(major: u64, minor: u64, patch: u64) -> Self {
        Self::from_parts(major, minor, patch, true)
    }

    const fn from_parts(major: u64, minor: u64, patch: u64, prerelease: bool) -> Self {
        Self {
            major,
            minor,
            patch,
            prerelease,
        }
    }
}

impl Ord for StableVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        (
            self.major,
            self.minor,
            self.patch,
            release_precedence(self.prerelease),
        )
            .cmp(&(
                other.major,
                other.minor,
                other.patch,
                release_precedence(other.prerelease),
            ))
    }
}

impl PartialOrd for StableVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl fmt::Display for StableVersion {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

const fn release_precedence(prerelease: bool) -> u8 {
    if prerelease { 0 } else { 1 }
}

pub(super) fn stable_version(value: &str) -> Result<StableVersion, String> {
    let trimmed = value.trim().trim_start_matches('v');
    if trimmed.split('+').next().unwrap_or(trimmed).contains('-') {
        return Err(format!("release version '{value}' is a prerelease"));
    }
    comparable_version(value)
}

fn comparable_version(value: &str) -> Result<StableVersion, String> {
    let trimmed = value.trim().trim_start_matches('v');
    let without_build = trimmed.split('+').next().unwrap_or(trimmed);
    let prerelease = without_build.contains('-');
    let core = trimmed
        .split('+')
        .next()
        .unwrap_or(trimmed)
        .split('-')
        .next()
        .unwrap_or(trimmed);
    let mut parts = core.split('.');
    let Some(major) = parts.next() else {
        return Err(format!("release version '{value}' is not semver"));
    };
    let Some(minor) = parts.next() else {
        return Err(format!("release version '{value}' is not semver"));
    };
    let Some(patch) = parts.next() else {
        return Err(format!("release version '{value}' is not semver"));
    };
    if parts.next().is_some() {
        return Err(format!("release version '{value}' is not semver"));
    }

    let major = parse_version_component(value, major)?;
    let minor = parse_version_component(value, minor)?;
    let patch = parse_version_component(value, patch)?;
    if prerelease {
        Ok(StableVersion::prerelease(major, minor, patch))
    } else {
        Ok(StableVersion::new(major, minor, patch))
    }
}

fn parse_version_component(value: &str, component: &str) -> Result<u64, String> {
    if component.is_empty()
        || !component
            .chars()
            .all(|character| character.is_ascii_digit())
    {
        return Err(format!("release version '{value}' is not semver"));
    }

    component
        .parse::<u64>()
        .map_err(|_| format!("release version '{value}' is not semver"))
}

pub(super) fn current_version() -> StableVersion {
    comparable_version(env!("CARGO_PKG_VERSION")).expect("Cargo package version must be semver")
}

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;