1use crate::error::Error;
6
7#[derive(Debug, Clone, PartialEq)]
9pub enum NodeSpec {
10 Exact(node_semver::Version),
11 Range(node_semver::Range),
12 Lts,
14 Latest,
16 LtsCodename(String),
19}
20
21impl NodeSpec {
22 pub fn parse(raw: &str) -> Result<NodeSpec, Error> {
25 let s = raw.trim();
26 let lowered = s.to_ascii_lowercase();
27 match lowered.as_str() {
28 "lts" | "lts/*" => return Ok(NodeSpec::Lts),
29 "latest" | "current" | "node" | "*" => return Ok(NodeSpec::Latest),
30 _ => {}
31 }
32 if let Some(name) = lowered.strip_prefix("lts/") {
33 if !name.is_empty() && name.chars().all(|c| c.is_ascii_alphabetic()) {
34 return Ok(NodeSpec::LtsCodename(name.to_string()));
35 }
36 return Err(Error::NoMatchingVersion {
37 requested: raw.to_string(),
38 platform_note: String::new(),
39 });
40 }
41 let unprefixed = s.strip_prefix('v').unwrap_or(s);
42 if let Ok(v) = node_semver::Version::parse(unprefixed) {
43 return Ok(NodeSpec::Exact(v));
44 }
45 if let Ok(r) = node_semver::Range::parse(unprefixed) {
46 return Ok(NodeSpec::Range(r));
47 }
48 if !lowered.is_empty() && lowered.chars().all(|c| c.is_ascii_alphabetic()) {
52 return Ok(NodeSpec::LtsCodename(lowered));
53 }
54 Err(Error::NoMatchingVersion {
55 requested: raw.to_string(),
56 platform_note: String::new(),
57 })
58 }
59
60 pub fn satisfied_by(&self, version: &node_semver::Version) -> Option<bool> {
65 match self {
66 NodeSpec::Exact(v) => Some(v == version),
67 NodeSpec::Range(r) => Some(version.satisfies(r)),
68 NodeSpec::Lts | NodeSpec::Latest | NodeSpec::LtsCodename(_) => None,
69 }
70 }
71
72 pub fn display(&self) -> String {
75 match self {
76 NodeSpec::Exact(v) => v.to_string(),
77 NodeSpec::Range(r) => r.to_string(),
78 NodeSpec::Lts => "lts".to_string(),
79 NodeSpec::Latest => "latest".to_string(),
80 NodeSpec::LtsCodename(name) => format!("lts/{name}"),
81 }
82 }
83}
84
85impl std::str::FromStr for NodeSpec {
86 type Err = Error;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 NodeSpec::parse(s)
90 }
91}
92
93impl std::fmt::Display for NodeSpec {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.write_str(&self.display())
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum RequestSource {
102 DevEngines,
104 NodeVersionFile,
106 Nvmrc,
108}
109
110impl RequestSource {
111 pub fn label(self) -> &'static str {
112 match self {
113 RequestSource::DevEngines => "devEngines.runtime",
114 RequestSource::NodeVersionFile => ".node-version",
115 RequestSource::Nvmrc => ".nvmrc",
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
123pub struct NodeRequest {
124 pub spec: NodeSpec,
125 pub raw: String,
130 pub on_fail: aube_manifest::OnFail,
131 pub source: RequestSource,
132 pub origin: std::path::PathBuf,
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn parse(s: &str) -> NodeSpec {
142 NodeSpec::parse(s).unwrap()
143 }
144
145 #[test]
146 fn parses_exact_versions() {
147 assert!(matches!(parse("22.1.0"), NodeSpec::Exact(_)));
148 assert!(matches!(parse("v22.1.0"), NodeSpec::Exact(_)));
149 assert!(matches!(parse("18.0.0-rc.1"), NodeSpec::Exact(_)));
150 }
151
152 #[test]
153 fn parses_ranges() {
154 assert!(matches!(parse("^22"), NodeSpec::Range(_)));
155 assert!(matches!(parse(">=18 <21"), NodeSpec::Range(_)));
156 assert!(matches!(parse("22"), NodeSpec::Range(_)));
157 assert!(matches!(parse("22.x"), NodeSpec::Range(_)));
158 assert!(matches!(parse("~18.12"), NodeSpec::Range(_)));
159 }
160
161 #[test]
162 fn parses_aliases() {
163 assert_eq!(parse("lts"), NodeSpec::Lts);
164 assert_eq!(parse("LTS"), NodeSpec::Lts);
165 assert_eq!(parse("lts/*"), NodeSpec::Lts);
166 assert_eq!(parse("latest"), NodeSpec::Latest);
167 assert_eq!(parse("current"), NodeSpec::Latest);
168 assert_eq!(parse("node"), NodeSpec::Latest);
169 assert_eq!(parse("*"), NodeSpec::Latest);
170 assert_eq!(parse("lts/jod"), NodeSpec::LtsCodename("jod".into()));
171 assert_eq!(parse("lts/Jod"), NodeSpec::LtsCodename("jod".into()));
172 assert_eq!(parse("jod"), NodeSpec::LtsCodename("jod".into()));
173 }
174
175 #[test]
176 fn rejects_garbage() {
177 assert!(NodeSpec::parse("lts/").is_err());
178 assert!(NodeSpec::parse("not a spec !!").is_err());
179 assert!(NodeSpec::parse("").is_err());
180 }
181
182 #[test]
183 fn local_satisfaction() {
184 let v: node_semver::Version = "22.3.0".parse().unwrap();
185 assert_eq!(parse("^22").satisfied_by(&v), Some(true));
186 assert_eq!(parse("^20").satisfied_by(&v), Some(false));
187 assert_eq!(parse("22.3.0").satisfied_by(&v), Some(true));
188 assert_eq!(parse("lts").satisfied_by(&v), None);
189 assert_eq!(parse("jod").satisfied_by(&v), None);
190 }
191
192 #[test]
193 fn bare_number_is_a_range() {
194 let v: node_semver::Version = "22.9.1".parse().unwrap();
196 assert_eq!(parse("22").satisfied_by(&v), Some(true));
197 }
198}