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 semver::VersionReq;
9use serde::{Deserialize, Serialize};
10use std::fmt::{Debug, Display};
11use std::str::FromStr;
12
13/// Represents an unresolved version or alias that must be resolved
14/// to a fully-qualified version.
15#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
16#[serde(untagged, into = "String", try_from = "String")]
17pub enum UnresolvedVersionSpec {
18    /// A special canary target.
19    Canary,
20    /// An alias that is used as a map to a version.
21    Alias(CompactString),
22    /// A partial version, requirement, or range (`^`, `~`, etc).
23    Req(VersionReq),
24    /// A list of requirements to match any against (joined by `||`).
25    ReqAny(Vec<VersionReq>),
26    /// A fully-qualified calendar version.
27    Calendar(CalVer),
28    /// A fully-qualified semantic version.
29    Semantic(SemVer),
30}
31
32impl UnresolvedVersionSpec {
33    /// Parse the provided string into an unresolved specification based
34    /// on the following rules, in order:
35    ///
36    /// - If the value "canary", map as `Canary` variant.
37    /// - If an alpha-numeric value that starts with a character, map as `Alias`.
38    /// - If contains `||`, split and parse each item with [`VersionReq`],
39    ///   and map as `ReqAny`.
40    /// - If contains `,` or ` ` (space), parse with [`VersionReq`], and map as `Req`.
41    /// - If starts with `=`, `^`, `~`, `>`, `<`, or `*`, parse with [`VersionReq`],
42    ///   and map as `Req`.
43    /// - Else parse as `Semantic` or `Calendar` types.
44    pub fn parse<T: AsRef<str>>(value: T) -> Result<Self, SpecError> {
45        Self::from_str(value.as_ref())
46    }
47
48    /// Return true if the provided alias matches the current specification.
49    pub fn is_alias<A: AsRef<str>>(&self, name: A) -> bool {
50        match self {
51            Self::Alias(alias) => alias == name.as_ref(),
52            _ => false,
53        }
54    }
55
56    /// Return true if the current specification is canary.
57    pub fn is_canary(&self) -> bool {
58        match self {
59            Self::Canary => true,
60            Self::Alias(alias) => alias == "canary",
61            _ => false,
62        }
63    }
64
65    /// Return true if the current specification is the "latest" alias.
66    pub fn is_latest(&self) -> bool {
67        match self {
68            Self::Alias(alias) => alias == "latest",
69            _ => false,
70        }
71    }
72
73    /// Convert the current unresolved specification to a resolved specification.
74    /// Note that this *does not* actually resolve or validate against a manifest,
75    /// and instead simply constructs the [`VersionSpec`].
76    ///
77    /// Furthermore, the `Req` and `ReqAny` variants will return a "latest" alias,
78    ///  as they are not resolved or valid versions.
79    pub fn to_resolved_spec(&self) -> VersionSpec {
80        match self {
81            Self::Canary => VersionSpec::Canary,
82            Self::Alias(alias) => VersionSpec::Alias(CompactString::new(alias)),
83            Self::Calendar(version) => VersionSpec::Calendar(version.to_owned()),
84            Self::Semantic(version) => VersionSpec::Semantic(version.to_owned()),
85            _ => VersionSpec::default(),
86        }
87    }
88}
89
90#[cfg(feature = "schematic")]
91impl schematic::Schematic for UnresolvedVersionSpec {
92    fn schema_name() -> Option<String> {
93        Some("UnresolvedVersionSpec".into())
94    }
95
96    fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
97        schema.set_description("Represents an unresolved version or alias that must be resolved to a fully-qualified version.");
98        schema.string_default()
99    }
100}
101
102impl Default for UnresolvedVersionSpec {
103    /// Returns a `latest` alias.
104    fn default() -> Self {
105        Self::Alias("latest".into())
106    }
107}
108
109impl FromStr for UnresolvedVersionSpec {
110    type Err = SpecError;
111
112    fn from_str(value: &str) -> Result<Self, Self::Err> {
113        if value == "canary" {
114            return Ok(UnresolvedVersionSpec::Canary);
115        }
116
117        let value = clean_version_string(value);
118
119        if is_alias_name(&value) {
120            return Ok(UnresolvedVersionSpec::Alias(CompactString::new(value)));
121        }
122
123        let value = clean_version_req_string(&value);
124
125        // OR requirements
126        if value.contains("||") {
127            let mut reqs = vec![];
128
129            for result in parse_multi(&value)? {
130                reqs.push(VersionReq::parse(&result)?);
131            }
132
133            return Ok(UnresolvedVersionSpec::ReqAny(reqs));
134        }
135
136        // Version or requirement
137        let (result, kind) = parse(value)?;
138
139        Ok(match kind {
140            ParseKind::Req => UnresolvedVersionSpec::Req(VersionReq::parse(&result)?),
141            ParseKind::Cal => UnresolvedVersionSpec::Calendar(CalVer::parse(&result)?),
142            _ => UnresolvedVersionSpec::Semantic(SemVer::parse(&result)?),
143        })
144    }
145}
146
147impl TryFrom<String> for UnresolvedVersionSpec {
148    type Error = SpecError;
149
150    fn try_from(value: String) -> Result<Self, Self::Error> {
151        Self::from_str(&value)
152    }
153}
154
155impl Into<String> for UnresolvedVersionSpec {
156    fn into(self) -> String {
157        self.to_string()
158    }
159}
160
161impl Display for UnresolvedVersionSpec {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            Self::Canary => write!(f, "canary"),
165            Self::Alias(alias) => write!(f, "{alias}"),
166            Self::Req(req) => write!(f, "{req}"),
167            Self::ReqAny(reqs) => write!(
168                f,
169                "{}",
170                reqs.iter()
171                    .map(|req| req.to_string())
172                    .collect::<Vec<_>>()
173                    .join(" || ")
174            ),
175            Self::Calendar(version) => write!(f, "{version}"),
176            Self::Semantic(version) => write!(f, "{version}"),
177        }
178    }
179}
180
181impl PartialEq<VersionSpec> for UnresolvedVersionSpec {
182    fn eq(&self, other: &VersionSpec) -> bool {
183        match (self, other) {
184            (Self::Canary, VersionSpec::Alias(a)) => a == "canary",
185            (Self::Alias(a1), VersionSpec::Alias(a2)) => a1 == a2,
186            (Self::Calendar(v1), VersionSpec::Calendar(v2)) => v1 == v2,
187            (Self::Semantic(v1), VersionSpec::Semantic(v2)) => v1 == v2,
188            _ => false,
189        }
190    }
191}
192
193impl AsRef<UnresolvedVersionSpec> for UnresolvedVersionSpec {
194    fn as_ref(&self) -> &UnresolvedVersionSpec {
195        self
196    }
197}