1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct NodeMajorVersion(u16);
10
11impl NodeMajorVersion {
12 pub const fn new(major: u16) -> Result<Self, NodeVersionParseError> {
18 if major == 0 {
19 Err(NodeVersionParseError::InvalidVersion)
20 } else {
21 Ok(Self(major))
22 }
23 }
24
25 #[must_use]
27 pub const fn get(self) -> u16 {
28 self.0
29 }
30}
31
32#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct NodeVersion {
35 major: NodeMajorVersion,
36 minor: Option<u16>,
37 patch: Option<u16>,
38}
39
40impl NodeVersion {
41 pub const fn new(
47 major: u16,
48 minor: Option<u16>,
49 patch: Option<u16>,
50 ) -> Result<Self, NodeVersionParseError> {
51 if minor.is_none() && patch.is_some() {
52 return Err(NodeVersionParseError::InvalidVersion);
53 }
54 match NodeMajorVersion::new(major) {
55 Ok(major) => Ok(Self {
56 major,
57 minor,
58 patch,
59 }),
60 Err(error) => Err(error),
61 }
62 }
63
64 #[must_use]
66 pub const fn major(self) -> u16 {
67 self.major.get()
68 }
69
70 #[must_use]
72 pub const fn minor(self) -> Option<u16> {
73 self.minor
74 }
75
76 #[must_use]
78 pub const fn patch(self) -> Option<u16> {
79 self.patch
80 }
81}
82
83impl fmt::Display for NodeVersion {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match (self.minor, self.patch) {
86 (Some(minor), Some(patch)) => write!(formatter, "{}.{}.{}", self.major(), minor, patch),
87 (Some(minor), None) => write!(formatter, "{}.{}", self.major(), minor),
88 (None, _) => write!(formatter, "{}", self.major()),
89 }
90 }
91}
92
93impl FromStr for NodeVersion {
94 type Err = NodeVersionParseError;
95
96 fn from_str(input: &str) -> Result<Self, Self::Err> {
97 parse_version(input)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum NodeVersionParseError {
104 Empty,
105 InvalidVersion,
106}
107
108impl fmt::Display for NodeVersionParseError {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 Self::Empty => formatter.write_str("Node.js version cannot be empty"),
112 Self::InvalidVersion => formatter.write_str("invalid Node.js version"),
113 }
114 }
115}
116
117impl Error for NodeVersionParseError {}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct NodeRuntime {
122 version: Option<NodeVersion>,
123}
124
125impl NodeRuntime {
126 #[must_use]
128 pub const fn new(version: Option<NodeVersion>) -> Self {
129 Self { version }
130 }
131
132 #[must_use]
134 pub const fn version(self) -> Option<NodeVersion> {
135 self.version
136 }
137}
138
139#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
141pub enum NodePackageManagerPreference {
142 Npm,
143 Pnpm,
144 Yarn,
145 Bun,
146}
147
148impl NodePackageManagerPreference {
149 #[must_use]
151 pub const fn as_str(self) -> &'static str {
152 match self {
153 Self::Npm => "npm",
154 Self::Pnpm => "pnpm",
155 Self::Yarn => "yarn",
156 Self::Bun => "bun",
157 }
158 }
159}
160
161fn parse_version(input: &str) -> Result<NodeVersion, NodeVersionParseError> {
162 let trimmed = input.trim().trim_start_matches('v');
163 if trimmed.is_empty() {
164 return Err(NodeVersionParseError::Empty);
165 }
166 let parts = trimmed.split('.').collect::<Vec<_>>();
167 if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
168 return Err(NodeVersionParseError::InvalidVersion);
169 }
170 let major = parse_part(parts[0])?;
171 let minor = parts.get(1).copied().map(parse_part).transpose()?;
172 let patch = parts.get(2).copied().map(parse_part).transpose()?;
173 NodeVersion::new(major, minor, patch)
174}
175
176fn parse_part(input: &str) -> Result<u16, NodeVersionParseError> {
177 input
178 .parse::<u16>()
179 .map_err(|_error| NodeVersionParseError::InvalidVersion)
180}
181
182#[cfg(test)]
183mod tests {
184 use super::{NodePackageManagerPreference, NodeRuntime, NodeVersion, NodeVersionParseError};
185
186 #[test]
187 fn parses_node_versions() -> Result<(), NodeVersionParseError> {
188 let version: NodeVersion = "v20.11.1".parse()?;
189 assert_eq!(version.major(), 20);
190 assert_eq!(version.minor(), Some(11));
191 assert_eq!(version.patch(), Some(1));
192 assert_eq!(version.to_string(), "20.11.1");
193 assert_eq!("20".parse::<NodeVersion>()?.major(), 20);
194 Ok(())
195 }
196
197 #[test]
198 fn stores_runtime_metadata() -> Result<(), NodeVersionParseError> {
199 let version: NodeVersion = "20".parse()?;
200 let runtime = NodeRuntime::new(Some(version));
201 assert_eq!(runtime.version().map(NodeVersion::major), Some(20));
202 assert_eq!(NodePackageManagerPreference::Pnpm.as_str(), "pnpm");
203 Ok(())
204 }
205}