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