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