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