Skip to main content

lux_lib/package/
version.rs

1use std::{
2    cmp::{self, Ordering},
3    fmt::Display,
4    str::FromStr,
5};
6
7use html_escape::decode_html_entities;
8use itertools::Itertools;
9
10use nonempty::NonEmpty;
11use semver::{Comparator, Error, Op, Version, VersionReq};
12use serde::{de, Deserialize, Deserializer, Serialize};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum VersionReqToVersionError {
17    #[error("cannot parse version from non-exact version requirement '{0}'")]
18    NonExactVersionReq(VersionReq),
19    #[error("cannot parse version from version requirement '*' (any version)")]
20    Any,
21}
22
23#[derive(Clone, Eq, PartialEq, Hash, Debug)]
24pub enum PackageVersion {
25    /// **SemVer version** as defined by <https://semver.org>,
26    /// but a bit more lenient for compatibility with luarocks
27    SemVer(SemVer),
28    /// A known **Dev** version
29    DevVer(DevVer),
30    /// An arbitrary string version.
31    /// Yes, luarocks-site allows arbitrary string versions in the root manifest
32    /// ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻
33    StringVer(StringVer),
34}
35
36impl HasModRev for PackageVersion {
37    fn to_modrev_string(&self) -> String {
38        match self {
39            Self::SemVer(ver) => ver.to_modrev_string(),
40            Self::DevVer(ver) => ver.to_modrev_string(),
41            Self::StringVer(ver) => ver.to_modrev_string(),
42        }
43    }
44}
45
46impl PackageVersion {
47    pub fn parse(text: &str) -> Result<Self, PackageVersionParseError> {
48        PackageVersion::from_str(text)
49    }
50    /// Note that this loses the specrev information.
51    pub fn into_version_req(&self) -> PackageVersionReq {
52        match self {
53            PackageVersion::DevVer(DevVer { modrev, .. }) => {
54                PackageVersionReq::DevVer(modrev.to_owned())
55            }
56            PackageVersion::StringVer(StringVer { modrev, .. }) => {
57                PackageVersionReq::StringVer(modrev.to_owned())
58            }
59            PackageVersion::SemVer(SemVer { version, .. }) => {
60                let version = version.to_owned();
61                PackageVersionReq::SemVer(VersionReq {
62                    comparators: vec![Comparator {
63                        op: Op::Exact,
64                        major: version.major,
65                        minor: Some(version.minor),
66                        patch: Some(version.patch),
67                        pre: version.pre,
68                    }],
69                })
70            }
71        }
72    }
73
74    pub(crate) fn is_semver(&self) -> bool {
75        matches!(self, PackageVersion::SemVer(_))
76    }
77
78    pub(crate) fn default_dev_version() -> Self {
79        Self::DevVer(DevVer::default())
80    }
81
82    pub(crate) fn default_dev_version_with_specrev(specrev: SpecRev) -> Self {
83        Self::DevVer(DevVer::default_with_specrev(specrev))
84    }
85}
86
87impl TryFrom<PackageVersionReq> for PackageVersion {
88    type Error = VersionReqToVersionError;
89
90    fn try_from(req: PackageVersionReq) -> Result<Self, Self::Error> {
91        match req {
92            PackageVersionReq::SemVer(version_req) => {
93                match NonEmpty::try_from(version_req.comparators.clone()) {
94                    Ok(comparators)
95                        if comparators
96                            .iter()
97                            .any(|comparator| comparator.op != semver::Op::Exact) =>
98                    {
99                        Err(VersionReqToVersionError::NonExactVersionReq(version_req))
100                    }
101                    Ok(comparators) => {
102                        let comparator = comparators.first();
103                        let version = semver::Version {
104                            major: comparator.major,
105                            minor: comparator.minor.unwrap_or(0),
106                            patch: comparator.patch.unwrap_or(0),
107                            pre: comparator.pre.clone(),
108                            build: semver::BuildMetadata::EMPTY,
109                        };
110                        let component_count = if comparator.patch.is_some() {
111                            3
112                        } else if comparator.minor.is_some() {
113                            2
114                        } else {
115                            1
116                        };
117                        Ok(PackageVersion::SemVer(SemVer {
118                            version,
119                            component_count,
120                            specrev: 1.into(),
121                        }))
122                    }
123                    Err(_) => Err(VersionReqToVersionError::NonExactVersionReq(version_req)),
124                }
125            }
126            PackageVersionReq::DevVer(modrev) => Ok(PackageVersion::DevVer(DevVer {
127                modrev,
128                specrev: 1.into(),
129            })),
130            PackageVersionReq::StringVer(modrev) => Ok(PackageVersion::StringVer(StringVer {
131                modrev,
132                specrev: 1.into(),
133            })),
134            PackageVersionReq::Any => Err(VersionReqToVersionError::Any),
135        }
136    }
137}
138
139#[derive(Error, Debug)]
140pub enum PackageVersionParseError {
141    #[error(transparent)]
142    Specrev(#[from] SpecrevParseError),
143    #[error("failed to parse version: {0}")]
144    Version(#[from] Error),
145}
146
147impl Serialize for PackageVersion {
148    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
149    where
150        S: serde::Serializer,
151    {
152        match self {
153            PackageVersion::SemVer(version) => version.serialize(serializer),
154            PackageVersion::DevVer(version) => version.serialize(serializer),
155            PackageVersion::StringVer(version) => version.serialize(serializer),
156        }
157    }
158}
159
160impl<'de> Deserialize<'de> for PackageVersion {
161    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162    where
163        D: Deserializer<'de>,
164    {
165        let s = String::deserialize(deserializer)?;
166        Self::from_str(&s).map_err(de::Error::custom)
167    }
168}
169
170impl Display for PackageVersion {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            PackageVersion::SemVer(version) => version.fmt(f),
174            PackageVersion::DevVer(version) => version.fmt(f),
175            PackageVersion::StringVer(version) => version.fmt(f),
176        }
177    }
178}
179
180impl PartialOrd for PackageVersion {
181    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
182        Some(self.cmp(other))
183    }
184}
185
186impl Ord for PackageVersion {
187    fn cmp(&self, other: &Self) -> Ordering {
188        match (self, other) {
189            (PackageVersion::SemVer(a), PackageVersion::SemVer(b)) => a.cmp(b),
190            (PackageVersion::SemVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
191            (PackageVersion::SemVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
192            (PackageVersion::DevVer(..), PackageVersion::SemVer(..)) => Ordering::Greater,
193            (PackageVersion::DevVer(a), PackageVersion::DevVer(b)) => a.cmp(b),
194            (PackageVersion::DevVer(..), PackageVersion::StringVer(..)) => Ordering::Greater,
195            (PackageVersion::StringVer(a), PackageVersion::StringVer(b)) => a.cmp(b),
196            (PackageVersion::StringVer(..), PackageVersion::SemVer(..)) => Ordering::Less,
197            (PackageVersion::StringVer(..), PackageVersion::DevVer(..)) => Ordering::Less,
198        }
199    }
200}
201
202impl FromStr for PackageVersion {
203    type Err = PackageVersionParseError;
204
205    fn from_str(text: &str) -> Result<Self, Self::Err> {
206        let (modrev, specrev) = split_specrev(text)?;
207        match modrev {
208            "scm" => Ok(PackageVersion::DevVer(DevVer {
209                modrev: DevVersion::Scm,
210                specrev,
211            })),
212            "dev" => Ok(PackageVersion::DevVer(DevVer {
213                modrev: DevVersion::Dev,
214                specrev,
215            })),
216            modrev => match parse_version(modrev) {
217                Ok(version) => Ok(PackageVersion::SemVer(SemVer {
218                    component_count: cmp::min(text.chars().filter(|c| *c == '.').count() + 1, 3),
219                    version,
220                    specrev,
221                })),
222                Err(_) => Ok(PackageVersion::StringVer(StringVer {
223                    modrev: modrev.into(),
224                    specrev,
225                })),
226            },
227        }
228    }
229}
230
231/// The revision of a rov
232#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
233pub struct SpecRev(u16);
234
235impl From<u16> for SpecRev {
236    fn from(value: u16) -> Self {
237        Self(value)
238    }
239}
240
241impl Display for SpecRev {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        self.0.fmt(f)
244    }
245}
246
247impl Default for SpecRev {
248    fn default() -> Self {
249        Self(1)
250    }
251}
252
253/// Iterates `SpecRev`s upwards, starting from `1`.
254pub(crate) struct SpecRevIterator {
255    current: u16,
256}
257
258impl SpecRevIterator {
259    pub fn new() -> Self {
260        SpecRevIterator {
261            current: SpecRev::default().0,
262        }
263    }
264}
265
266impl Iterator for SpecRevIterator {
267    type Item = SpecRev;
268
269    fn next(&mut self) -> Option<Self::Item> {
270        let current = self.current;
271        self.current += 1;
272        if self.current == 0 {
273            None // overflow
274        } else {
275            Some(SpecRev(current))
276        }
277    }
278}
279
280// TODO: Stop deriving Eq here
281#[derive(Clone, Eq, PartialEq, Hash, Debug)]
282pub struct SemVer {
283    version: Version,
284    component_count: usize,
285    specrev: SpecRev,
286}
287
288impl HasModRev for SemVer {
289    fn to_modrev_string(&self) -> String {
290        self.version.to_string()
291    }
292}
293
294impl Display for SemVer {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        let (version_str, remainder) = split_semver_version(&self.version.to_string());
297        let mut luarocks_version_str = version_str.split('.').take(self.component_count).join(".");
298        if let Some(remainder) = remainder {
299            // luarocks allows and arbitrary number of '.' separators
300            // We treat anything after the third '.' as a semver prerelease/build version,
301            // so we have to convert it back for luarocks.
302            luarocks_version_str.push_str(&format!(".{remainder}"));
303        }
304        let str = format!("{}-{}", luarocks_version_str, self.specrev);
305        str.fmt(f)
306    }
307}
308
309fn split_semver_version(version_str: &str) -> (String, Option<String>) {
310    if let Some(pos) = version_str.rfind('-') {
311        if let Some(pre_build_str) = version_str.get(pos + 1..) {
312            (version_str[..pos].into(), Some(pre_build_str.into()))
313        } else {
314            (version_str[..pos].into(), None)
315        }
316    } else {
317        (version_str.into(), None)
318    }
319}
320
321impl Serialize for SemVer {
322    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
323    where
324        S: serde::Serializer,
325    {
326        self.to_string().serialize(serializer)
327    }
328}
329
330impl Ord for SemVer {
331    fn cmp(&self, other: &Self) -> Ordering {
332        let result = self.version.cmp(&other.version);
333        if result == Ordering::Equal {
334            return self.specrev.cmp(&other.specrev);
335        }
336        result
337    }
338}
339
340impl PartialOrd for SemVer {
341    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
342        Some(self.cmp(other))
343    }
344}
345
346#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, Default)]
347#[serde(rename_all = "lowercase")]
348pub enum DevVersion {
349    #[default]
350    Dev,
351    Scm,
352}
353
354impl Display for DevVersion {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        match self {
357            Self::Dev => "dev".fmt(f),
358            Self::Scm => "scm".fmt(f),
359        }
360    }
361}
362
363#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
364pub struct DevVer {
365    modrev: DevVersion,
366    specrev: SpecRev,
367}
368
369impl DevVer {
370    fn default_with_specrev(specrev: SpecRev) -> Self {
371        Self {
372            modrev: DevVersion::default(),
373            specrev,
374        }
375    }
376}
377
378impl HasModRev for DevVer {
379    fn to_modrev_string(&self) -> String {
380        self.modrev.to_string().to_lowercase()
381    }
382}
383
384impl Display for DevVer {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        let str = format!("{}-{}", self.modrev, self.specrev);
387        str.fmt(f)
388    }
389}
390
391impl Serialize for DevVer {
392    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
393    where
394        S: serde::Serializer,
395    {
396        self.to_string().serialize(serializer)
397    }
398}
399
400impl Ord for DevVer {
401    fn cmp(&self, other: &Self) -> Ordering {
402        // NOTE: We compare specrevs first for dev versions
403        let result = self.specrev.cmp(&other.specrev);
404        if result == Ordering::Equal {
405            return self.modrev.cmp(&other.modrev);
406        }
407        result
408    }
409}
410
411impl PartialOrd for DevVer {
412    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
413        Some(self.cmp(other))
414    }
415}
416
417#[derive(Clone, Eq, PartialEq, Hash, Debug)]
418pub struct StringVer {
419    modrev: String,
420    specrev: SpecRev,
421}
422
423impl HasModRev for StringVer {
424    fn to_modrev_string(&self) -> String {
425        self.modrev.to_string()
426    }
427}
428
429impl Display for StringVer {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        let str = format!("{}-{}", self.modrev, self.specrev);
432        str.fmt(f)
433    }
434}
435
436impl Serialize for StringVer {
437    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
438    where
439        S: serde::Serializer,
440    {
441        self.to_string().serialize(serializer)
442    }
443}
444
445impl Ord for StringVer {
446    fn cmp(&self, other: &Self) -> Ordering {
447        // NOTE: We compare specrevs first for dev versions
448        let result = self.specrev.cmp(&other.specrev);
449        if result == Ordering::Equal {
450            return self.modrev.cmp(&other.modrev);
451        }
452        result
453    }
454}
455
456impl PartialOrd for StringVer {
457    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
458        Some(self.cmp(other))
459    }
460}
461
462pub(crate) trait HasModRev {
463    /// If a version has a modrev (and possibly a specrev),
464    /// this is equivalent to `to_string()`, but includes only the modrev.
465    fn to_modrev_string(&self) -> String;
466}
467
468#[derive(Error, Debug)]
469#[error(transparent)]
470pub struct PackageVersionReqError(#[from] Error);
471
472/// **SemVer version** requirement as defined by <https://semver.org>.
473/// or a **Dev** version requirement, which can be one of "dev", "scm", or "git"
474#[derive(Clone, Eq, PartialEq, Hash, Debug)]
475pub enum PackageVersionReq {
476    /// A PackageVersionReq that matches a SemVer version.
477    SemVer(VersionReq),
478    /// A PackageVersionReq that matches only known dev versions.
479    DevVer(DevVersion),
480    /// A PackageVersionReq that matches a arbitrary string version.
481    StringVer(String),
482    /// A PackageVersionReq that has no version constraint.
483    Any,
484}
485
486impl PackageVersionReq {
487    /// Returns a `PackageVersionReq` that matches any version.
488    pub fn any() -> Self {
489        PackageVersionReq::Any
490    }
491
492    pub fn parse(text: &str) -> Result<Self, PackageVersionReqError> {
493        PackageVersionReq::from_str(text)
494    }
495
496    pub fn matches(&self, version: &PackageVersion) -> bool {
497        match (self, version) {
498            (PackageVersionReq::SemVer(req), PackageVersion::SemVer(ver)) => {
499                req.matches(&ver.version)
500            }
501            (PackageVersionReq::DevVer(req), PackageVersion::DevVer(ver)) => req == &ver.modrev,
502            (PackageVersionReq::StringVer(req), PackageVersion::StringVer(ver)) => {
503                req == &ver.modrev
504            }
505            (PackageVersionReq::Any, _) => true,
506            _ => false,
507        }
508    }
509
510    pub fn is_any(&self) -> bool {
511        matches!(self, PackageVersionReq::Any)
512    }
513}
514
515impl Display for PackageVersionReq {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        match self {
518            PackageVersionReq::SemVer(version_req) => {
519                let mut str = version_req.to_string();
520                if str.starts_with("=") {
521                    str = str.replacen("=", "==", 1);
522                } else if str.starts_with("^") {
523                    str = str.replacen("^", "~>", 1);
524                }
525                str.fmt(f)
526            }
527            PackageVersionReq::DevVer(name_req) => write!(f, "=={}", &name_req),
528            PackageVersionReq::StringVer(name_req) => write!(f, "=={}", &name_req),
529            PackageVersionReq::Any => f.write_str("any"),
530        }
531    }
532}
533
534impl<'de> Deserialize<'de> for PackageVersionReq {
535    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
536    where
537        D: Deserializer<'de>,
538    {
539        String::deserialize(deserializer)?
540            .parse()
541            .map_err(serde::de::Error::custom)
542    }
543}
544
545impl FromStr for PackageVersionReq {
546    type Err = PackageVersionReqError;
547
548    fn from_str(text: &str) -> Result<Self, Self::Err> {
549        let text = correct_version_req_str(text);
550
551        let trimmed = text.trim_start_matches('=').trim_start_matches('@').trim();
552
553        match parse_version_req(&text) {
554            Ok(_) => Ok(PackageVersionReq::SemVer(parse_version_req(&text)?)),
555            Err(_) => match trimmed {
556                "scm" => Ok(PackageVersionReq::DevVer(DevVersion::Scm)),
557                "dev" => Ok(PackageVersionReq::DevVer(DevVersion::Dev)),
558                ver => Ok(PackageVersionReq::StringVer(ver.to_string())),
559            },
560        }
561    }
562}
563
564fn correct_version_req_str(text: &str) -> String {
565    text.chars()
566        .chunk_by(|t| t.is_alphanumeric() || matches!(t, '-' | '_' | '.'))
567        .into_iter()
568        .map(|(is_version_str, chars)| (is_version_str, chars.collect::<String>()))
569        .map(|(is_version_str, chunk)| {
570            if is_version_str && !is_known_dev_version_str(&chunk) {
571                let version_str = trim_specrev(&chunk);
572                correct_prerelease_version_string(version_str)
573            } else {
574                chunk
575            }
576        })
577        .collect::<String>()
578}
579
580fn trim_specrev(version_str: &str) -> &str {
581    if let Some(pos) = version_str.rfind('-') {
582        &version_str[..pos]
583    } else {
584        version_str
585    }
586}
587
588#[derive(Error, Debug)]
589pub enum SpecrevParseError {
590    #[error("specrev {specrev} in version {full_version} contains non-numeric characters")]
591    InvalidSpecrev {
592        specrev: String,
593        full_version: String,
594    },
595    #[error("could not parse specrev in version {0}")]
596    InvalidVersion(String),
597}
598
599fn split_specrev(version_str: &str) -> Result<(&str, SpecRev), SpecrevParseError> {
600    if let Some(pos) = version_str.rfind('-') {
601        if let Some(specrev_str) = version_str.get(pos + 1..) {
602            if specrev_str.chars().all(|c| c.is_ascii_digit()) {
603                let specrev =
604                    specrev_str
605                        .parse::<u16>()
606                        .map_err(|_| SpecrevParseError::InvalidSpecrev {
607                            specrev: specrev_str.into(),
608                            full_version: version_str.into(),
609                        })?;
610                Ok((&version_str[..pos], specrev.into()))
611            } else {
612                Err(SpecrevParseError::InvalidSpecrev {
613                    specrev: specrev_str.into(),
614                    full_version: version_str.into(),
615                })
616            }
617        } else {
618            Err(SpecrevParseError::InvalidVersion(version_str.into()))
619        }
620    } else {
621        // We assume a specrev of 1 if none can be found.
622        Ok((version_str, 1.into()))
623    }
624}
625
626fn is_known_dev_version_str(text: &str) -> bool {
627    matches!(text, "dev" | "scm")
628}
629
630/// Parses a Version from a string, automatically supplying any missing details (i.e. missing
631/// minor/patch sections).
632fn parse_version(s: &str) -> Result<Version, Error> {
633    let version_str = correct_version_string(s);
634    Version::parse(&version_str)
635}
636
637/// Transform LuaRocks constraints into constraints that can be parsed by the semver crate.
638fn parse_version_req(version_constraints: &str) -> Result<VersionReq, Error> {
639    let unescaped = decode_html_entities(version_constraints)
640        .to_string()
641        .as_str()
642        .to_owned();
643    let transformed = match unescaped {
644        s if s.starts_with("~>") => parse_pessimistic_version_constraint(s)?,
645        s if s.starts_with("@") => format!("={}", &s[1..]),
646        // The semver crate only understands "= version", unlike luarocks which understands "== version".
647        s if s.starts_with("==") => s[1..].to_string(),
648        s if s // semver parses no constraint prefix as ^ (equivalent to ~>)
649            .find(|c: char| c.is_alphanumeric())
650            .is_some_and(|idx| idx == 0) =>
651        {
652            format!("={}", &s)
653        }
654        s => s,
655    };
656
657    let version_req = VersionReq::parse(&transformed)?;
658    Ok(version_req)
659}
660
661fn parse_pessimistic_version_constraint(version_constraint: String) -> Result<String, Error> {
662    // pessimistic operator
663    let min_version_str = &version_constraint[2..].trim();
664    let min_version = Version::parse(&correct_version_string(min_version_str))?;
665
666    let max_version = match min_version_str.matches('.').count() {
667        0 => Version {
668            major: &min_version.major + 1,
669            ..min_version.clone()
670        },
671        1 => Version {
672            minor: &min_version.minor + 1,
673            ..min_version.clone()
674        },
675        _ => Version {
676            patch: &min_version.patch + 1,
677            ..min_version.clone()
678        },
679    };
680
681    Ok(format!(">= {min_version}, < {max_version}"))
682}
683
684/// ┻━┻ ︵╰(°□°╰) Luarocks allows for an arbitrary number of version digits
685/// This function attempts to correct a non-semver compliant version string,
686/// by swapping the third '.' out with a '-', converting the non-semver
687/// compliant digits to a pre-release identifier.
688fn correct_version_string(version: &str) -> String {
689    let version = append_minor_patch_if_missing(version);
690    correct_prerelease_version_string(&version)
691}
692
693fn correct_prerelease_version_string(version: &str) -> String {
694    let parts: Vec<&str> = version.split('.').collect();
695    if parts.len() > 3 {
696        let corrected_version = format!(
697            "{}.{}.{}-{}",
698            parts[0],
699            parts[1],
700            parts[2],
701            parts[3..].join(".")
702        );
703        corrected_version
704    } else {
705        version.to_string()
706    }
707}
708
709/// Recursively append .0 until the version string has a minor or patch version
710fn append_minor_patch_if_missing(version: &str) -> String {
711    if version.matches('.').count() < 2 {
712        append_minor_patch_if_missing(&format!("{version}.0"))
713    } else {
714        version.to_string()
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    #[tokio::test]
723    async fn parse_semver_version() {
724        assert_eq!(
725            PackageVersion::parse("1-1").unwrap(),
726            PackageVersion::SemVer(SemVer {
727                version: "1.0.0".parse().unwrap(),
728                component_count: 1,
729                specrev: 1.into(),
730            })
731        );
732        assert_eq!(
733            PackageVersion::parse("1.0-1").unwrap(),
734            PackageVersion::SemVer(SemVer {
735                version: "1.0.0".parse().unwrap(),
736                component_count: 2,
737                specrev: 1.into(),
738            })
739        );
740        assert_eq!(
741            PackageVersion::parse("1.0.0-1").unwrap(),
742            PackageVersion::SemVer(SemVer {
743                version: "1.0.0".parse().unwrap(),
744                component_count: 3,
745                specrev: 1.into()
746            })
747        );
748        assert_eq!(
749            PackageVersion::parse("1.0.0-1").unwrap(),
750            PackageVersion::SemVer(SemVer {
751                version: "1.0.0".parse().unwrap(),
752                component_count: 3,
753                specrev: 1.into()
754            })
755        );
756        assert_eq!(
757            PackageVersion::parse("1.0.0-10-1").unwrap(),
758            PackageVersion::SemVer(SemVer {
759                version: "1.0.0-10".parse().unwrap(),
760                component_count: 3,
761                specrev: 1.into()
762            })
763        );
764        assert_eq!(
765            PackageVersion::parse("1.0.0.10-1").unwrap(),
766            PackageVersion::SemVer(SemVer {
767                version: "1.0.0-10".parse().unwrap(),
768                component_count: 3,
769                specrev: 1.into()
770            })
771        );
772        assert_eq!(
773            PackageVersion::parse("1.0.0.10.0-1").unwrap(),
774            PackageVersion::SemVer(SemVer {
775                version: "1.0.0-10.0".parse().unwrap(),
776                component_count: 3,
777                specrev: 1.into()
778            })
779        );
780    }
781
782    #[tokio::test]
783    async fn parse_dev_version() {
784        assert_eq!(
785            PackageVersion::parse("dev-1").unwrap(),
786            PackageVersion::DevVer(DevVer {
787                modrev: DevVersion::Dev,
788                specrev: 1.into()
789            })
790        );
791        assert_eq!(
792            PackageVersion::parse("scm-1").unwrap(),
793            PackageVersion::DevVer(DevVer {
794                modrev: DevVersion::Scm,
795                specrev: 1.into()
796            })
797        );
798        assert_eq!(
799            PackageVersion::parse("git-1").unwrap(),
800            PackageVersion::StringVer(StringVer {
801                modrev: "git".into(),
802                specrev: 1.into()
803            })
804        );
805        assert_eq!(
806            PackageVersion::parse("scm-1").unwrap(),
807            PackageVersion::DevVer(DevVer {
808                modrev: DevVersion::Scm,
809                specrev: 1.into()
810            })
811        );
812    }
813
814    #[tokio::test]
815    async fn parse_dev_version_req() {
816        assert_eq!(
817            PackageVersionReq::parse("dev").unwrap(),
818            PackageVersionReq::DevVer(DevVersion::Dev)
819        );
820        assert_eq!(
821            PackageVersionReq::parse("scm").unwrap(),
822            PackageVersionReq::DevVer(DevVersion::Scm)
823        );
824        assert_eq!(
825            PackageVersionReq::parse("git").unwrap(),
826            PackageVersionReq::StringVer("git".into())
827        );
828        assert_eq!(
829            PackageVersionReq::parse("==dev").unwrap(),
830            PackageVersionReq::DevVer(DevVersion::Dev)
831        );
832        assert_eq!(
833            PackageVersionReq::parse("==git").unwrap(),
834            PackageVersionReq::StringVer("git".into())
835        );
836        assert_eq!(
837            PackageVersionReq::parse("== dev").unwrap(),
838            PackageVersionReq::DevVer(DevVersion::Dev)
839        );
840        assert_eq!(
841            PackageVersionReq::parse("== scm").unwrap(),
842            PackageVersionReq::DevVer(DevVersion::Scm)
843        );
844        assert_eq!(
845            PackageVersionReq::parse("@dev").unwrap(),
846            PackageVersionReq::DevVer(DevVersion::Dev)
847        );
848        assert_eq!(
849            PackageVersionReq::parse("@git").unwrap(),
850            PackageVersionReq::StringVer("git".into())
851        );
852        assert_eq!(
853            PackageVersionReq::parse("@ dev").unwrap(),
854            PackageVersionReq::DevVer(DevVersion::Dev)
855        );
856        assert_eq!(
857            PackageVersionReq::parse("@ scm").unwrap(),
858            PackageVersionReq::DevVer(DevVersion::Scm)
859        );
860        assert_eq!(
861            PackageVersionReq::parse(">1-1,<1.2-2").unwrap(),
862            PackageVersionReq::SemVer(">1,<1.2".parse().unwrap())
863        );
864        assert_eq!(
865            PackageVersionReq::parse("> 1-1, < 1.2-2").unwrap(),
866            PackageVersionReq::SemVer("> 1, < 1.2".parse().unwrap())
867        );
868        assert_eq!(
869            PackageVersionReq::parse("> 2.1.0.10, < 2.1.1").unwrap(),
870            PackageVersionReq::SemVer("> 2.1.0-10, < 2.1.1".parse().unwrap())
871        );
872    }
873
874    #[tokio::test]
875    async fn package_version_req_semver_roundtrips() {
876        let req = PackageVersionReq::parse("==0.7.1").unwrap();
877        assert_eq!(req.to_string(), "==0.7.1");
878
879        let req = PackageVersionReq::parse("0.7.1").unwrap();
880        assert_eq!(req.to_string(), "==0.7.1");
881
882        let req = PackageVersionReq::parse(">=0.7.1").unwrap();
883        assert_eq!(req.to_string(), ">=0.7.1");
884
885        let req = PackageVersionReq::parse(">0.7.1").unwrap();
886        assert_eq!(req.to_string(), ">0.7.1");
887
888        let req = PackageVersionReq::parse("<0.7.1").unwrap();
889        assert_eq!(req.to_string(), "<0.7.1");
890
891        let req = PackageVersionReq::parse("~> 0.7.1").unwrap();
892        assert_eq!(req.to_string(), ">=0.7.1, <0.7.2");
893    }
894
895    #[tokio::test]
896    async fn package_version_req_devver_roundtrips() {
897        let req = PackageVersionReq::parse("==scm").unwrap();
898        assert_eq!(req.to_string(), "==scm");
899
900        let req = PackageVersionReq::parse("@scm").unwrap();
901        assert_eq!(req.to_string(), "==scm");
902
903        let req = PackageVersionReq::parse("scm").unwrap();
904        assert_eq!(req.to_string(), "==scm");
905
906        let req = PackageVersionReq::parse("==a144124839f027a2d0a95791936c478d047126fc").unwrap();
907        assert_eq!(
908            req.to_string(),
909            "==a144124839f027a2d0a95791936c478d047126fc"
910        );
911    }
912}