#![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 BunVersion {
major: u16,
minor: Option<u16>,
patch: Option<u16>,
}
impl BunVersion {
pub const fn new(
major: u16,
minor: Option<u16>,
patch: Option<u16>,
) -> Result<Self, BunVersionParseError> {
if major == 0 || (minor.is_none() && patch.is_some()) {
Err(BunVersionParseError::InvalidVersion)
} else {
Ok(Self {
major,
minor,
patch,
})
}
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
}
impl FromStr for BunVersion {
type Err = BunVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_bun_version(input)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BunVersionParseError {
Empty,
InvalidVersion,
}
impl fmt::Display for BunVersionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Bun version cannot be empty"),
Self::InvalidVersion => formatter.write_str("invalid Bun version"),
}
}
}
impl Error for BunVersionParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum BunCommand {
Install,
Run,
Test,
Build,
}
impl BunCommand {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Install => "install",
Self::Run => "run",
Self::Test => "test",
Self::Build => "build",
}
}
}
impl FromStr for BunCommand {
type Err = BunCommandParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(BunCommandParseError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"install" | "i" => Ok(Self::Install),
"run" => Ok(Self::Run),
"test" => Ok(Self::Test),
"build" => Ok(Self::Build),
_ => Err(BunCommandParseError::Unknown),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BunCommandParseError {
Empty,
Unknown,
}
impl fmt::Display for BunCommandParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Bun command cannot be empty"),
Self::Unknown => formatter.write_str("unknown Bun command"),
}
}
}
impl Error for BunCommandParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum BunLockfile {
Text,
Binary,
}
impl BunLockfile {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Text => "bun.lock",
Self::Binary => "bun.lockb",
}
}
}
fn parse_bun_version(input: &str) -> Result<BunVersion, BunVersionParseError> {
let trimmed = input.trim().trim_start_matches('v');
if trimmed.is_empty() {
return Err(BunVersionParseError::Empty);
}
let parts = trimmed.split('.').collect::<Vec<_>>();
if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
return Err(BunVersionParseError::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()?;
BunVersion::new(major, minor, patch)
}
fn parse_part(input: &str) -> Result<u16, BunVersionParseError> {
input
.parse::<u16>()
.map_err(|_error| BunVersionParseError::InvalidVersion)
}
#[cfg(test)]
mod tests {
use super::{BunCommand, BunLockfile, BunVersion};
#[test]
fn parses_bun_metadata() -> Result<(), Box<dyn std::error::Error>> {
let version: BunVersion = "1.1.8".parse()?;
assert_eq!(version.major(), 1);
assert_eq!("install".parse::<BunCommand>()?, BunCommand::Install);
assert_eq!(BunLockfile::Binary.as_str(), "bun.lockb");
Ok(())
}
}