Skip to main content

version_spec/
unresolved_spec.rs

1#![allow(clippy::from_over_into)]
2
3use crate::spec_error::SpecError;
4use crate::unresolved_parser::*;
5use crate::version_types::*;
6use crate::{VersionSpec, clean_version_req_string, clean_version_string, is_alias_name};
7use compact_str::CompactString;
8use human_sort::compare;
9use semver::Prerelease;
10use semver::VersionReq;
11use serde::{Deserialize, Serialize};
12use std::cmp::Ordering;
13use std::fmt::{Debug, Display};
14use std::str::FromStr;
15
16/// Represents an unresolved version or alias that must be resolved
17/// to a fully-qualified version.
18#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
19#[serde(untagged, into = "String", try_from = "String")]
20pub enum UnresolvedVersionSpec {
21    /// A special canary target.
22    Canary,
23    /// An alias that is used as a map to a version.
24    Alias(CompactString),
25    /// A partial version, requirement, or range (`^`, `~`, etc).
26    Req(VersionReq),
27    /// A list of requirements to match any against (joined by `||`).
28    ReqAny(Vec<VersionReq>),
29    /// A fully-qualified calendar version.
30    Calendar(CalVer),
31    /// A fully-qualified semantic version.
32    Semantic(SemVer),
33}
34
35impl UnresolvedVersionSpec {
36    /// Parse the provided string into an unresolved specification based
37    /// on the following rules, in order:
38    ///
39    /// - If the value "canary", map as `Canary` variant.
40    /// - If an alpha-numeric value that starts with a character, map as `Alias`.
41    /// - If contains `||`, split and parse each item with [`VersionReq`],
42    ///   and map as `ReqAny`.
43    /// - If contains `,` or ` ` (space), parse with [`VersionReq`], and map as `Req`.
44    /// - If starts with `=`, `^`, `~`, `>`, `<`, or `*`, parse with [`VersionReq`],
45    ///   and map as `Req`.
46    /// - Else parse as `Semantic` or `Calendar` types.
47    pub fn parse<T: AsRef<str>>(value: T) -> Result<Self, SpecError> {
48        Self::from_str(value.as_ref())
49    }
50
51    /// Return true if the provided alias matches the current specification.
52    pub fn is_alias<A: AsRef<str>>(&self, name: A) -> bool {
53        match self {
54            Self::Alias(alias) => alias == name.as_ref(),
55            _ => false,
56        }
57    }
58
59    /// Return true if the current specification is canary.
60    pub fn is_canary(&self) -> bool {
61        match self {
62            Self::Canary => true,
63            Self::Alias(alias) => alias == "canary",
64            _ => false,
65        }
66    }
67
68    /// Return true if the current specification can be treated as a
69    /// fully qualified version, either calendar or semantic.
70    pub fn is_fully_qualified(&self) -> bool {
71        matches!(self, Self::Calendar(_) | Self::Semantic(_))
72    }
73
74    /// Return true if the current specification is the "latest" alias.
75    pub fn is_latest(&self) -> bool {
76        match self {
77            Self::Alias(alias) => alias == "latest",
78            _ => false,
79        }
80    }
81
82    /// Convert the current unresolved specification to a resolved specification.
83    /// Note that this *does not* actually resolve or validate against a manifest,
84    /// and instead simply constructs the [`VersionSpec`].
85    ///
86    /// Furthermore, the `Req` and `ReqAny` variants will return a "latest" alias,
87    ///  as they are not resolved or valid versions.
88    pub fn to_resolved_spec(&self) -> VersionSpec {
89        match self {
90            Self::Canary => VersionSpec::Canary,
91            Self::Alias(alias) => VersionSpec::Alias(CompactString::new(alias)),
92            Self::Calendar(version) => VersionSpec::Calendar(version.to_owned()),
93            Self::Semantic(version) => VersionSpec::Semantic(version.to_owned()),
94            _ => VersionSpec::default(),
95        }
96    }
97
98    /// Convert the current unresolved specification to a partial string, where
99    /// minor and patch versions are omitted if not defined, and the comparator
100    /// operator is also omitted. For example, "~1.2" would simply print "1.2".
101    ///
102    /// Furthermore, `Canary` will return "canary", `ReqAny` will return "latest",
103    /// and aliases will return as-is.
104    pub fn to_partial_string(&self) -> String {
105        fn from_parts(
106            major: u64,
107            minor: Option<u64>,
108            patch: Option<u64>,
109            pre: &Prerelease,
110        ) -> String {
111            let mut version = format!("{major}");
112
113            minor.inspect(|m| {
114                version.push_str(&format!(".{m}"));
115            });
116
117            patch.inspect(|p| {
118                version.push_str(&format!(".{p}"));
119            });
120
121            if !pre.is_empty() {
122                version.push('-');
123                version.push_str(pre.as_str());
124            }
125
126            version
127        }
128
129        match self {
130            UnresolvedVersionSpec::Canary => "canary".into(),
131            UnresolvedVersionSpec::Alias(alias) => alias.to_string(),
132            UnresolvedVersionSpec::Req(req) => {
133                let req = req.comparators.first().unwrap();
134
135                from_parts(req.major, req.minor, req.patch, &req.pre)
136            }
137            UnresolvedVersionSpec::ReqAny(_) => "latest".into(),
138            UnresolvedVersionSpec::Calendar(ver) => {
139                from_parts(ver.major, Some(ver.minor), Some(ver.patch), &ver.pre)
140            }
141            UnresolvedVersionSpec::Semantic(ver) => {
142                from_parts(ver.major, Some(ver.minor), Some(ver.patch), &ver.pre)
143            }
144        }
145    }
146}
147
148#[cfg(feature = "schematic")]
149impl schematic::Schematic for UnresolvedVersionSpec {
150    fn schema_name() -> Option<String> {
151        Some("UnresolvedVersionSpec".into())
152    }
153
154    fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
155        schema.set_description("Represents an unresolved version or alias that must be resolved to a fully-qualified version.");
156        schema.string_default()
157    }
158}
159
160impl Default for UnresolvedVersionSpec {
161    /// Returns a `latest` alias.
162    fn default() -> Self {
163        Self::Alias("latest".into())
164    }
165}
166
167impl FromStr for UnresolvedVersionSpec {
168    type Err = SpecError;
169
170    fn from_str(value: &str) -> Result<Self, Self::Err> {
171        if value == "canary" {
172            return Ok(UnresolvedVersionSpec::Canary);
173        }
174
175        let value = clean_version_string(value);
176
177        if is_alias_name(&value) {
178            return Ok(UnresolvedVersionSpec::Alias(CompactString::new(value)));
179        }
180
181        let value = clean_version_req_string(&value);
182
183        // OR requirements
184        if value.contains("||") {
185            let mut reqs = vec![];
186
187            for result in parse_multi(&value)? {
188                reqs.push(VersionReq::parse(&result)?);
189            }
190
191            return Ok(UnresolvedVersionSpec::ReqAny(reqs));
192        }
193
194        // Version or requirement
195        let (result, kind) = parse(value)?;
196
197        Ok(match kind {
198            ParseKind::Req => UnresolvedVersionSpec::Req(VersionReq::parse(&result)?),
199            ParseKind::Cal => UnresolvedVersionSpec::Calendar(CalVer::parse(&result)?),
200            _ => UnresolvedVersionSpec::Semantic(SemVer::parse(&result)?),
201        })
202    }
203}
204
205impl TryFrom<String> for UnresolvedVersionSpec {
206    type Error = SpecError;
207
208    fn try_from(value: String) -> Result<Self, Self::Error> {
209        Self::from_str(&value)
210    }
211}
212
213impl Into<String> for UnresolvedVersionSpec {
214    fn into(self) -> String {
215        self.to_string()
216    }
217}
218
219impl Display for UnresolvedVersionSpec {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Self::Canary => write!(f, "canary"),
223            Self::Alias(alias) => write!(f, "{alias}"),
224            Self::Req(req) => write!(f, "{req}"),
225            Self::ReqAny(reqs) => write!(
226                f,
227                "{}",
228                reqs.iter()
229                    .map(|req| req.to_string())
230                    .collect::<Vec<_>>()
231                    .join(" || ")
232            ),
233            Self::Calendar(version) => write!(f, "{version}"),
234            Self::Semantic(version) => write!(f, "{version}"),
235        }
236    }
237}
238
239impl PartialEq<VersionSpec> for UnresolvedVersionSpec {
240    fn eq(&self, other: &VersionSpec) -> bool {
241        match (self, other) {
242            (Self::Canary, VersionSpec::Canary) => true,
243            (Self::Canary, VersionSpec::Alias(a)) => a == "canary",
244            (Self::Alias(a1), VersionSpec::Alias(a2)) => a1 == a2,
245            (Self::Calendar(v1), VersionSpec::Calendar(v2)) => v1 == v2,
246            (Self::Semantic(v1), VersionSpec::Semantic(v2)) => v1 == v2,
247            _ => false,
248        }
249    }
250}
251
252impl AsRef<UnresolvedVersionSpec> for UnresolvedVersionSpec {
253    fn as_ref(&self) -> &UnresolvedVersionSpec {
254        self
255    }
256}
257
258impl PartialOrd<UnresolvedVersionSpec> for UnresolvedVersionSpec {
259    fn partial_cmp(&self, other: &UnresolvedVersionSpec) -> Option<Ordering> {
260        Some(self.cmp(other))
261    }
262}
263
264impl Ord for UnresolvedVersionSpec {
265    fn cmp(&self, other: &Self) -> Ordering {
266        match (self, other) {
267            (Self::Canary, Self::Canary) => Ordering::Equal,
268            (Self::Alias(l), Self::Alias(r)) => l.cmp(r),
269            (Self::Calendar(l), Self::Calendar(r)) => l.cmp(r),
270            (Self::Semantic(l), Self::Semantic(r)) => l.cmp(r),
271            _ => compare(&self.to_string(), &other.to_string()),
272        }
273    }
274}