Skip to main content

version_spec/
resolved_spec.rs

1#![allow(clippy::from_over_into)]
2
3use crate::spec_error::SpecError;
4use crate::{UnresolvedVersionSpec, clean_version_string, is_alias_name, is_calver};
5use crate::{is_semver, version_types::*};
6use compact_str::CompactString;
7use semver::Version;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::str::FromStr;
11
12/// Represents a resolved version or alias.
13#[derive(Clone, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
14#[serde(untagged, into = "String", try_from = "String")]
15pub enum VersionSpec {
16    /// A special canary target.
17    Canary,
18    /// An alias that is used as a map to a version.
19    Alias(CompactString),
20    /// A fully-qualified calendar version.
21    Calendar(CalVer),
22    /// A fully-qualified semantic version.
23    Semantic(SemVer),
24}
25
26impl VersionSpec {
27    /// Parse the provided string into a resolved specification based
28    /// on the following rules, in order:
29    ///
30    /// - If the value "canary", map as `Canary` variant.
31    /// - If an alpha-numeric value that starts with a character, map as `Alias`.
32    /// - Else parse with [`Version`], and map as `Version`.
33    pub fn parse<T: AsRef<str>>(value: T) -> Result<Self, SpecError> {
34        Self::from_str(value.as_ref())
35    }
36
37    /// Return the specification as a resolved [`Version`].
38    pub fn as_version(&self) -> Option<&Version> {
39        match self {
40            Self::Calendar(inner) => Some(&inner.0),
41            Self::Semantic(inner) => Some(&inner.0),
42            _ => None,
43        }
44    }
45
46    /// Return true if the provided alias matches the current specification.
47    pub fn is_alias<A: AsRef<str>>(&self, name: A) -> bool {
48        match self {
49            Self::Alias(alias) => alias == name.as_ref(),
50            _ => false,
51        }
52    }
53
54    /// Return true if the current specification is canary.
55    pub fn is_canary(&self) -> bool {
56        match self {
57            Self::Canary => true,
58            Self::Alias(alias) => alias == "canary",
59            _ => false,
60        }
61    }
62
63    /// Return true if the current specification is the "latest" alias.
64    pub fn is_latest(&self) -> bool {
65        match self {
66            Self::Alias(alias) => alias == "latest",
67            _ => false,
68        }
69    }
70
71    /// Convert the current resolved specification to an unresolved specification.
72    pub fn to_unresolved_spec(&self) -> UnresolvedVersionSpec {
73        match self {
74            Self::Canary => UnresolvedVersionSpec::Canary,
75            Self::Alias(alias) => UnresolvedVersionSpec::Alias(alias.to_owned()),
76            Self::Calendar(version) => UnresolvedVersionSpec::Calendar(version.to_owned()),
77            Self::Semantic(version) => UnresolvedVersionSpec::Semantic(version.to_owned()),
78        }
79    }
80}
81
82#[cfg(feature = "schematic")]
83impl schematic::Schematic for VersionSpec {
84    fn schema_name() -> Option<String> {
85        Some("VersionSpec".into())
86    }
87
88    fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
89        schema.set_description("Represents a resolved version or alias.");
90        schema.string_default()
91    }
92}
93
94impl Default for VersionSpec {
95    /// Returns a `latest` alias.
96    fn default() -> Self {
97        Self::Alias("latest".into())
98    }
99}
100
101impl FromStr for VersionSpec {
102    type Err = SpecError;
103
104    fn from_str(value: &str) -> Result<Self, Self::Err> {
105        if value == "canary" {
106            return Ok(VersionSpec::Canary);
107        }
108
109        let value = clean_version_string(value);
110
111        if is_alias_name(&value) {
112            return Ok(VersionSpec::Alias(CompactString::new(value)));
113        }
114
115        if is_calver(&value) {
116            return Ok(VersionSpec::Calendar(CalVer::parse(&value)?));
117        }
118
119        if is_semver(&value) {
120            return Ok(VersionSpec::Semantic(SemVer::parse(&value)?));
121        }
122
123        Err(SpecError::UnknownResolvedFormat(value.to_owned()))
124    }
125}
126
127impl TryFrom<String> for VersionSpec {
128    type Error = SpecError;
129
130    fn try_from(value: String) -> Result<Self, Self::Error> {
131        Self::from_str(&value)
132    }
133}
134
135impl Into<String> for VersionSpec {
136    fn into(self) -> String {
137        self.to_string()
138    }
139}
140
141impl fmt::Debug for VersionSpec {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        // Debug version as a string instead of a struct
144        write!(f, "{self}")
145    }
146}
147
148impl fmt::Display for VersionSpec {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::Canary => write!(f, "canary"),
152            Self::Alias(alias) => write!(f, "{alias}"),
153            Self::Calendar(version) => write!(f, "{version}"),
154            Self::Semantic(version) => write!(f, "{version}"),
155        }
156    }
157}
158
159impl PartialEq<&str> for VersionSpec {
160    fn eq(&self, other: &&str) -> bool {
161        match self {
162            Self::Canary => "canary" == *other,
163            Self::Alias(alias) => alias == other,
164            _ => &self.to_string() == other,
165        }
166    }
167}
168
169impl PartialEq<semver::Version> for VersionSpec {
170    fn eq(&self, other: &semver::Version) -> bool {
171        match self {
172            Self::Calendar(version) => &version.0 == other,
173            Self::Semantic(version) => &version.0 == other,
174            _ => false,
175        }
176    }
177}
178
179impl AsRef<VersionSpec> for VersionSpec {
180    fn as_ref(&self) -> &VersionSpec {
181        self
182    }
183}