Skip to main content

use_node_js/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Node.js major version metadata.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct NodeMajorVersion(u16);
10
11impl NodeMajorVersion {
12    /// Creates a Node.js major version.
13    ///
14    /// # Errors
15    ///
16    /// Returns [`NodeVersionParseError::InvalidVersion`] when `major` is zero.
17    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    /// Returns the major version number.
26    #[must_use]
27    pub const fn get(self) -> u16 {
28        self.0
29    }
30}
31
32/// Node.js version metadata.
33#[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    /// Creates Node.js version metadata.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`NodeVersionParseError::InvalidVersion`] when the major version is invalid.
46    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    /// Returns the major version number.
65    #[must_use]
66    pub const fn major(self) -> u16 {
67        self.major.get()
68    }
69
70    /// Returns the optional minor version number.
71    #[must_use]
72    pub const fn minor(self) -> Option<u16> {
73        self.minor
74    }
75
76    /// Returns the optional patch version number.
77    #[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/// Error returned while parsing a Node.js version.
102#[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/// Node.js runtime metadata.
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct NodeRuntime {
122    version: Option<NodeVersion>,
123}
124
125impl NodeRuntime {
126    /// Creates runtime metadata with an optional version.
127    #[must_use]
128    pub const fn new(version: Option<NodeVersion>) -> Self {
129        Self { version }
130    }
131
132    /// Returns the optional version.
133    #[must_use]
134    pub const fn version(self) -> Option<NodeVersion> {
135        self.version
136    }
137}
138
139/// Preferred package manager for a Node-oriented project.
140#[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    /// Returns the package manager label.
150    #[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}