snm_core/version/
user_version.rs

1use semver::VersionReq;
2use serde::Deserialize;
3use std::{env::current_dir, fmt::Display, fs::read_to_string, str::FromStr};
4
5use crate::{
6    fetcher::{Lts, Release},
7    types::{UserAlias, UserLts},
8    SnmRes,
9};
10
11use super::{DistVersion, ParseVersion};
12
13const PACKAGE_JSON: &str = "package.json";
14const VERSION_FILES: [&str; 3] = [".nvmrc", ".node-version", PACKAGE_JSON];
15
16/// `UserVersion` represents the user provided version
17/// It could be alias, lts codename, partial or full semver
18#[derive(Debug, PartialEq, Eq)]
19pub enum UserVersion {
20    /// Only major segment ie. 18
21    Major(u64),
22    /// Major and Minor segment ie. 12.3
23    MajorMinor(u64, u64),
24    /// Full semver ie. 14.17.4
25    Semver(DistVersion),
26    /// Range semver ie. >14.14 | <=12.3
27    Range(VersionReq),
28    /// Alias name ie. latest, lts
29    Alias(UserAlias),
30    /// LTS codename ie. fermium, erbium
31    Lts(UserLts),
32}
33
34impl ParseVersion for UserVersion {
35    type Item = Self;
36    fn parse(v: &str) -> SnmRes<Self::Item> {
37        let trimmed = v.trim_start_matches('v');
38
39        let version = if let Ok(x) = DistVersion::parse(trimmed) {
40            Self::Semver(x)
41        } else {
42            let is_numeric = trimmed.chars().next().unwrap_or_default().is_numeric();
43
44            if is_numeric {
45                let mut splitted = trimmed.splitn(2, '.');
46
47                match (splitted.next(), splitted.next()) {
48                    (Some(a), Some(b)) => Self::MajorMinor(a.parse::<u64>()?, b.parse::<u64>()?),
49                    (Some(a), None) => Self::Major(a.parse::<u64>()?),
50                    _ => anyhow::bail!("Unable to parse the user provided version"),
51                }
52            } else if let Ok(x) = VersionReq::parse(v) {
53                Self::Range(x)
54            } else if UserLts::is_lts(v) {
55                Self::Lts(UserLts::new(v))
56            } else {
57                // from_str method is used bcz it returns err if the given alias is same as active alias
58                Self::Alias(UserAlias::from_str(v)?)
59            }
60        };
61
62        Ok(version)
63    }
64}
65
66// NOTE: this is used by `clap` to parse user input
67impl FromStr for UserVersion {
68    type Err = anyhow::Error;
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        Self::parse(s)
71    }
72}
73
74impl Display for UserVersion {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::Major(major) => write!(f, "v{}.x.x", major),
78            Self::MajorMinor(major, minor) => write!(f, "v{}.{}.x", major, minor),
79            Self::Semver(x) => x.fmt(f),
80            Self::Range(x) => x.fmt(f),
81            Self::Alias(x) => x.fmt(f),
82            Self::Lts(x) => x.fmt(f),
83        }
84    }
85}
86
87#[derive(Debug, Deserialize)]
88pub struct Engines {
89    node: Option<String>,
90}
91
92#[derive(Debug, Deserialize)]
93pub struct PkgJson {
94    engines: Option<Engines>,
95}
96
97impl UserVersion {
98    pub fn match_release(&self, release: &Release) -> bool {
99        match (self, &release.version, &release.lts) {
100            (Self::Major(a), DistVersion(b), _) => a == &b.major,
101            (Self::MajorMinor(a, b), DistVersion(c), _) => a == &c.major && b == &c.minor,
102            (Self::Semver(a), b, _) => a.eq(b),
103            (Self::Range(a), DistVersion(b), _) => a.matches(b),
104            (Self::Lts(a), _, Lts::Yes(b)) => a.as_ref() == b,
105            _ => false,
106        }
107    }
108
109    pub fn from_file() -> SnmRes<Self> {
110        let pwd = current_dir()?;
111
112        for f_name in VERSION_FILES {
113            let f_path = pwd.join(&f_name);
114
115            if !f_path.exists() {
116                continue;
117            }
118
119            let version_file = read_to_string(&f_path)?;
120
121            if f_name.eq(PACKAGE_JSON) {
122                let pkg_json: PkgJson = serde_json::from_str(&version_file)?;
123
124                if let Some(Engines { node: Some(v) }) = pkg_json.engines {
125                    let parsed = Self::parse(&v)?;
126
127                    return Ok(parsed);
128                }
129            } else {
130                let line = version_file.lines().next();
131
132                if let Some(l) = line {
133                    let parsed = Self::parse(l)?;
134
135                    return Ok(parsed);
136                }
137            }
138        }
139
140        anyhow::bail!("Unable to read version from dotfiles. Please provide a version manually.")
141    }
142}