#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NodeMajorVersion(u16);
impl NodeMajorVersion {
pub const fn new(major: u16) -> Result<Self, NodeVersionParseError> {
if major == 0 {
Err(NodeVersionParseError::InvalidVersion)
} else {
Ok(Self(major))
}
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NodeVersion {
major: NodeMajorVersion,
minor: Option<u16>,
patch: Option<u16>,
}
impl NodeVersion {
pub const fn new(
major: u16,
minor: Option<u16>,
patch: Option<u16>,
) -> Result<Self, NodeVersionParseError> {
if minor.is_none() && patch.is_some() {
return Err(NodeVersionParseError::InvalidVersion);
}
match NodeMajorVersion::new(major) {
Ok(major) => Ok(Self {
major,
minor,
patch,
}),
Err(error) => Err(error),
}
}
#[must_use]
pub const fn major(self) -> u16 {
self.major.get()
}
#[must_use]
pub const fn minor(self) -> Option<u16> {
self.minor
}
#[must_use]
pub const fn patch(self) -> Option<u16> {
self.patch
}
}
impl fmt::Display for NodeVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match (self.minor, self.patch) {
(Some(minor), Some(patch)) => write!(formatter, "{}.{}.{}", self.major(), minor, patch),
(Some(minor), None) => write!(formatter, "{}.{}", self.major(), minor),
(None, _) => write!(formatter, "{}", self.major()),
}
}
}
impl FromStr for NodeVersion {
type Err = NodeVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_version(input)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NodeVersionParseError {
Empty,
InvalidVersion,
}
impl fmt::Display for NodeVersionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Node.js version cannot be empty"),
Self::InvalidVersion => formatter.write_str("invalid Node.js version"),
}
}
}
impl Error for NodeVersionParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NodeRuntime {
version: Option<NodeVersion>,
}
impl NodeRuntime {
#[must_use]
pub const fn new(version: Option<NodeVersion>) -> Self {
Self { version }
}
#[must_use]
pub const fn version(self) -> Option<NodeVersion> {
self.version
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum NodePackageManagerPreference {
Npm,
Pnpm,
Yarn,
Bun,
}
impl NodePackageManagerPreference {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Pnpm => "pnpm",
Self::Yarn => "yarn",
Self::Bun => "bun",
}
}
}
fn parse_version(input: &str) -> Result<NodeVersion, NodeVersionParseError> {
let trimmed = input.trim().trim_start_matches('v');
if trimmed.is_empty() {
return Err(NodeVersionParseError::Empty);
}
let parts = trimmed.split('.').collect::<Vec<_>>();
if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
return Err(NodeVersionParseError::InvalidVersion);
}
let major = parse_part(parts[0])?;
let minor = parts.get(1).copied().map(parse_part).transpose()?;
let patch = parts.get(2).copied().map(parse_part).transpose()?;
NodeVersion::new(major, minor, patch)
}
fn parse_part(input: &str) -> Result<u16, NodeVersionParseError> {
input
.parse::<u16>()
.map_err(|_error| NodeVersionParseError::InvalidVersion)
}
#[cfg(test)]
mod tests {
use super::{NodePackageManagerPreference, NodeRuntime, NodeVersion, NodeVersionParseError};
#[test]
fn parses_node_versions() -> Result<(), NodeVersionParseError> {
let version: NodeVersion = "v20.11.1".parse()?;
assert_eq!(version.major(), 20);
assert_eq!(version.minor(), Some(11));
assert_eq!(version.patch(), Some(1));
assert_eq!(version.to_string(), "20.11.1");
assert_eq!("20".parse::<NodeVersion>()?.major(), 20);
Ok(())
}
#[test]
fn stores_runtime_metadata() -> Result<(), NodeVersionParseError> {
let version: NodeVersion = "20".parse()?;
let runtime = NodeRuntime::new(Some(version));
assert_eq!(runtime.version().map(NodeVersion::major), Some(20));
assert_eq!(NodePackageManagerPreference::Pnpm.as_str(), "pnpm");
Ok(())
}
}