fundaia 0.7.2

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
//! Whether one version is newer than another, and nothing else.
//!
//! Separate from the checking and from the installing because this is the whole
//! decision — newer, older, the same, or unreadable — and it is worth being able
//! to test it without a terminal, a network or a file to overwrite.

/// A version, as much of one as a comparison needs.
///
/// The derived ordering is the whole implementation, which is why the fields
/// are in this order: major, then minor, then patch, and finally a release
/// outranking its own pre-releases — `false` sorts below `true`, and
/// `0.4.0-rc1` is indeed older than `0.4.0`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
    major: u64,
    minor: u64,
    patch: u64,
    release: bool,
}

impl Version {
    pub fn parse(text: &str) -> Option<Self> {
        let text = text.trim();
        let (core, tail) = match text.find(['-', '+']) {
            Some(at) => text.split_at(at),
            None => (text, ""),
        };

        let mut parts = core.split('.');
        let major = parts.next()?.parse().ok()?;
        let minor = parts.next().unwrap_or("0").parse().ok()?;
        let patch = parts.next().unwrap_or("0").parse().ok()?;

        // A fourth component means this is not the kind of version string this
        // understands, and guessing at it is how a tool nags about a downgrade.
        if parts.next().is_some() {
            return None;
        }

        Some(Self {
            major,
            minor,
            patch,
            // `+build` is still a release; only `-pre` is not.
            release: !tail.starts_with('-'),
        })
    }
}

/// The published version, handed back only when it really is ahead of this
/// build. Unreadable on either side means no: a tool that cannot compare two
/// numbers has no business replacing itself over the answer.
pub fn newer<'a>(current: &str, published: &'a str) -> Option<&'a str> {
    let installed = Version::parse(current)?;
    let candidate = Version::parse(published)?;

    (candidate > installed).then_some(published)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_read_a_plain_version() {
        assert_eq!(Version::parse("0.3.0").unwrap().minor, 3);
    }

    #[test]
    fn it_should_rank_a_newer_minor_above_an_older_one() {
        assert!(Version::parse("0.4.0") > Version::parse("0.3.9"));
    }

    #[test]
    fn it_should_rank_a_newer_major_above_a_much_higher_minor() {
        assert!(Version::parse("1.0.0") > Version::parse("0.99.0"));
    }

    #[test]
    fn it_should_rank_a_release_above_its_own_pre_release() {
        assert!(Version::parse("0.4.0") > Version::parse("0.4.0-rc1"));
    }

    #[test]
    fn it_should_read_a_version_carrying_build_metadata_as_a_release() {
        assert!(Version::parse("0.4.0+deadbeef").unwrap().release);
    }

    #[test]
    fn it_should_refuse_a_version_with_a_fourth_part() {
        assert!(Version::parse("1.2.3.4").is_none());
    }

    #[test]
    fn it_should_refuse_something_that_is_not_a_version_at_all() {
        assert!(Version::parse("latest").is_none());
    }

    #[test]
    fn it_should_report_a_newer_release() {
        assert_eq!(newer("0.3.0", "0.4.0"), Some("0.4.0"));
    }

    #[test]
    fn it_should_report_nothing_when_the_installed_version_is_the_newest() {
        assert!(newer("0.3.0", "0.3.0").is_none());
    }

    #[test]
    fn it_should_report_nothing_when_the_installed_version_is_ahead() {
        assert!(newer("0.4.0", "0.3.0").is_none());
    }

    #[test]
    fn it_should_report_nothing_when_nothing_has_been_cached_yet() {
        assert!(newer("0.3.0", "").is_none());
    }

    #[test]
    fn it_should_report_nothing_when_the_registry_answer_is_unreadable() {
        assert!(newer("0.3.0", "not-a-version").is_none());
    }
}