version_spec 0.11.3

A specification for working with partial, full, or aliased versions. Supports semver and calver.
Documentation
#![allow(clippy::from_over_into)]

use crate::spec_error::SpecError;
use crate::syntax::Version;
use crate::syntax_parser::parse_alias;
use crate::unresolved_spec::UnresolvedVersionSpec;
use compact_str::CompactString;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

/// Represents a resolved version or alias.
#[derive(Clone, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(untagged, into = "String", try_from = "String")]
pub enum VersionSpec {
    /// A special canary target.
    Canary,
    /// An alias that is used as a map to a version.
    Alias(CompactString),
    /// A fully-qualified version.
    Version(Version),
}

impl VersionSpec {
    /// Parse the provided string into a resolved specification based
    /// on the following rules, in order:
    ///
    /// - If the value "canary", map as `Canary` variant.
    /// - If an alpha-numeric value that starts with a character, map as `Alias`.
    /// - Else parse with [`Version`], and map as `Version`.
    pub fn parse<T: AsRef<str>>(value: T) -> Result<Self, SpecError> {
        Self::from_str(value.as_ref())
    }

    /// Return the specification as a resolved [`Version`].
    pub fn as_version(&self) -> Option<&Version> {
        match self {
            Self::Version(inner) => Some(inner),
            _ => None,
        }
    }

    /// Return the version scope if available.
    pub fn get_scope(&self) -> Option<&str> {
        match self {
            Self::Version(version) => version.scope.as_deref(),
            _ => None,
        }
    }

    /// Return true if the provided alias matches the current specification.
    pub fn is_alias<A: AsRef<str>>(&self, name: A) -> bool {
        match self {
            Self::Alias(alias) => alias == name.as_ref(),
            _ => false,
        }
    }

    /// Return true if the current specification is canary.
    pub fn is_canary(&self) -> bool {
        match self {
            Self::Canary => true,
            Self::Alias(alias) => alias == "canary",
            _ => false,
        }
    }

    /// Return true if the current specification is the "latest" alias.
    pub fn is_latest(&self) -> bool {
        match self {
            Self::Alias(alias) => alias == "latest",
            _ => false,
        }
    }

    /// Set the scope on either the current version, if applicable.
    pub fn set_scope(&mut self, scope: impl AsRef<str>) {
        if let Self::Version(version) = self {
            version.scope = Some(scope.as_ref().into());
        }
    }

    /// Convert the current resolved specification to an unresolved specification.
    pub fn to_unresolved_spec(&self) -> UnresolvedVersionSpec {
        match self {
            Self::Canary => UnresolvedVersionSpec::Canary,
            Self::Alias(alias) => UnresolvedVersionSpec::Alias(alias.to_owned()),
            Self::Version(version) => UnresolvedVersionSpec::Version(version.to_owned()),
        }
    }
}

#[cfg(feature = "schematic")]
impl schematic::Schematic for VersionSpec {
    fn schema_name() -> Option<String> {
        Some("VersionSpec".into())
    }

    fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
        schema.set_description("Represents a resolved version or alias.");
        schema.string_default()
    }
}

impl Default for VersionSpec {
    /// Returns a `latest` alias.
    fn default() -> Self {
        Self::Alias("latest".into())
    }
}

impl FromStr for VersionSpec {
    type Err = SpecError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value == "canary" {
            return Ok(Self::Canary);
        }

        // A version takes priority, with an alias being the residual:
        // whatever the grammar does not accept as a version
        match Version::parse(value) {
            Ok(version) => Ok(Self::Version(version)),
            Err(error) => match parse_alias(value) {
                Ok(alias) => Ok(Self::Alias(alias)),
                Err(_) => Err(error),
            },
        }
    }
}

impl TryFrom<String> for VersionSpec {
    type Error = SpecError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value)
    }
}

impl Into<String> for VersionSpec {
    fn into(self) -> String {
        self.to_string()
    }
}

impl fmt::Debug for VersionSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Debug version as a string instead of a struct
        write!(f, "{self}")
    }
}

impl fmt::Display for VersionSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Canary => write!(f, "canary"),
            Self::Alias(alias) => write!(f, "{alias}"),
            Self::Version(version) => write!(f, "{version}"),
        }
    }
}

impl PartialEq<&str> for VersionSpec {
    fn eq(&self, other: &&str) -> bool {
        match self {
            Self::Canary => "canary" == *other,
            Self::Alias(alias) => alias == other,
            _ => &self.to_string() == other,
        }
    }
}

impl PartialEq<Version> for VersionSpec {
    fn eq(&self, other: &Version) -> bool {
        match self {
            Self::Version(version) => version == other,
            _ => false,
        }
    }
}

impl AsRef<VersionSpec> for VersionSpec {
    fn as_ref(&self) -> &VersionSpec {
        self
    }
}