use crate::error::Error;
#[derive(Debug, Clone, PartialEq)]
pub enum NodeSpec {
Exact(node_semver::Version),
Range(node_semver::Range),
Lts,
Latest,
LtsCodename(String),
}
impl NodeSpec {
pub fn parse(raw: &str) -> Result<NodeSpec, Error> {
let s = raw.trim();
let lowered = s.to_ascii_lowercase();
match lowered.as_str() {
"lts" | "lts/*" => return Ok(NodeSpec::Lts),
"latest" | "current" | "node" | "*" => return Ok(NodeSpec::Latest),
_ => {}
}
if let Some(name) = lowered.strip_prefix("lts/") {
if !name.is_empty() && name.chars().all(|c| c.is_ascii_alphabetic()) {
return Ok(NodeSpec::LtsCodename(name.to_string()));
}
return Err(Error::NoMatchingVersion {
requested: raw.to_string(),
platform_note: String::new(),
});
}
let unprefixed = s.strip_prefix('v').unwrap_or(s);
if let Ok(v) = node_semver::Version::parse(unprefixed) {
return Ok(NodeSpec::Exact(v));
}
if let Ok(r) = node_semver::Range::parse(unprefixed) {
return Ok(NodeSpec::Range(r));
}
if !lowered.is_empty() && lowered.chars().all(|c| c.is_ascii_alphabetic()) {
return Ok(NodeSpec::LtsCodename(lowered));
}
Err(Error::NoMatchingVersion {
requested: raw.to_string(),
platform_note: String::new(),
})
}
pub fn satisfied_by(&self, version: &node_semver::Version) -> Option<bool> {
match self {
NodeSpec::Exact(v) => Some(v == version),
NodeSpec::Range(r) => Some(version.satisfies(r)),
NodeSpec::Lts | NodeSpec::Latest | NodeSpec::LtsCodename(_) => None,
}
}
pub fn display(&self) -> String {
match self {
NodeSpec::Exact(v) => v.to_string(),
NodeSpec::Range(r) => r.to_string(),
NodeSpec::Lts => "lts".to_string(),
NodeSpec::Latest => "latest".to_string(),
NodeSpec::LtsCodename(name) => format!("lts/{name}"),
}
}
}
impl std::str::FromStr for NodeSpec {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
NodeSpec::parse(s)
}
}
impl std::fmt::Display for NodeSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestSource {
DevEngines,
NodeVersionFile,
Nvmrc,
}
impl RequestSource {
pub fn label(self) -> &'static str {
match self {
RequestSource::DevEngines => "devEngines.runtime",
RequestSource::NodeVersionFile => ".node-version",
RequestSource::Nvmrc => ".nvmrc",
}
}
}
#[derive(Debug, Clone)]
pub struct NodeRequest {
pub spec: NodeSpec,
pub raw: String,
pub on_fail: aube_manifest::OnFail,
pub source: RequestSource,
pub origin: std::path::PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(s: &str) -> NodeSpec {
NodeSpec::parse(s).unwrap()
}
#[test]
fn parses_exact_versions() {
assert!(matches!(parse("22.1.0"), NodeSpec::Exact(_)));
assert!(matches!(parse("v22.1.0"), NodeSpec::Exact(_)));
assert!(matches!(parse("18.0.0-rc.1"), NodeSpec::Exact(_)));
}
#[test]
fn parses_ranges() {
assert!(matches!(parse("^22"), NodeSpec::Range(_)));
assert!(matches!(parse(">=18 <21"), NodeSpec::Range(_)));
assert!(matches!(parse("22"), NodeSpec::Range(_)));
assert!(matches!(parse("22.x"), NodeSpec::Range(_)));
assert!(matches!(parse("~18.12"), NodeSpec::Range(_)));
}
#[test]
fn parses_aliases() {
assert_eq!(parse("lts"), NodeSpec::Lts);
assert_eq!(parse("LTS"), NodeSpec::Lts);
assert_eq!(parse("lts/*"), NodeSpec::Lts);
assert_eq!(parse("latest"), NodeSpec::Latest);
assert_eq!(parse("current"), NodeSpec::Latest);
assert_eq!(parse("node"), NodeSpec::Latest);
assert_eq!(parse("*"), NodeSpec::Latest);
assert_eq!(parse("lts/jod"), NodeSpec::LtsCodename("jod".into()));
assert_eq!(parse("lts/Jod"), NodeSpec::LtsCodename("jod".into()));
assert_eq!(parse("jod"), NodeSpec::LtsCodename("jod".into()));
}
#[test]
fn rejects_garbage() {
assert!(NodeSpec::parse("lts/").is_err());
assert!(NodeSpec::parse("not a spec !!").is_err());
assert!(NodeSpec::parse("").is_err());
}
#[test]
fn local_satisfaction() {
let v: node_semver::Version = "22.3.0".parse().unwrap();
assert_eq!(parse("^22").satisfied_by(&v), Some(true));
assert_eq!(parse("^20").satisfied_by(&v), Some(false));
assert_eq!(parse("22.3.0").satisfied_by(&v), Some(true));
assert_eq!(parse("lts").satisfied_by(&v), None);
assert_eq!(parse("jod").satisfied_by(&v), None);
}
#[test]
fn bare_number_is_a_range() {
let v: node_semver::Version = "22.9.1".parse().unwrap();
assert_eq!(parse("22").satisfied_by(&v), Some(true));
}
}