Skip to main content

use_bun/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Bun version metadata.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct BunVersion {
10    major: u16,
11    minor: Option<u16>,
12    patch: Option<u16>,
13}
14
15impl BunVersion {
16    /// Creates Bun version metadata.
17    ///
18    /// # Errors
19    ///
20    /// Returns [`BunVersionParseError::InvalidVersion`] when the major version is zero.
21    pub const fn new(
22        major: u16,
23        minor: Option<u16>,
24        patch: Option<u16>,
25    ) -> Result<Self, BunVersionParseError> {
26        if major == 0 || (minor.is_none() && patch.is_some()) {
27            Err(BunVersionParseError::InvalidVersion)
28        } else {
29            Ok(Self {
30                major,
31                minor,
32                patch,
33            })
34        }
35    }
36
37    /// Returns the major version.
38    #[must_use]
39    pub const fn major(self) -> u16 {
40        self.major
41    }
42}
43
44impl FromStr for BunVersion {
45    type Err = BunVersionParseError;
46
47    fn from_str(input: &str) -> Result<Self, Self::Err> {
48        parse_bun_version(input)
49    }
50}
51
52/// Error returned while parsing a Bun version.
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum BunVersionParseError {
55    Empty,
56    InvalidVersion,
57}
58
59impl fmt::Display for BunVersionParseError {
60    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::Empty => formatter.write_str("Bun version cannot be empty"),
63            Self::InvalidVersion => formatter.write_str("invalid Bun version"),
64        }
65    }
66}
67
68impl Error for BunVersionParseError {}
69
70/// Common Bun command labels.
71#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub enum BunCommand {
73    Install,
74    Run,
75    Test,
76    Build,
77}
78
79impl BunCommand {
80    /// Returns the command label.
81    #[must_use]
82    pub const fn as_str(self) -> &'static str {
83        match self {
84            Self::Install => "install",
85            Self::Run => "run",
86            Self::Test => "test",
87            Self::Build => "build",
88        }
89    }
90}
91
92impl FromStr for BunCommand {
93    type Err = BunCommandParseError;
94
95    fn from_str(input: &str) -> Result<Self, Self::Err> {
96        let trimmed = input.trim();
97        if trimmed.is_empty() {
98            return Err(BunCommandParseError::Empty);
99        }
100        match trimmed.to_ascii_lowercase().as_str() {
101            "install" | "i" => Ok(Self::Install),
102            "run" => Ok(Self::Run),
103            "test" => Ok(Self::Test),
104            "build" => Ok(Self::Build),
105            _ => Err(BunCommandParseError::Unknown),
106        }
107    }
108}
109
110/// Error returned while parsing Bun command labels.
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum BunCommandParseError {
113    Empty,
114    Unknown,
115}
116
117impl fmt::Display for BunCommandParseError {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Empty => formatter.write_str("Bun command cannot be empty"),
121            Self::Unknown => formatter.write_str("unknown Bun command"),
122        }
123    }
124}
125
126impl Error for BunCommandParseError {}
127
128/// Common Bun lockfile labels.
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub enum BunLockfile {
131    Text,
132    Binary,
133}
134
135impl BunLockfile {
136    /// Returns the lockfile label.
137    #[must_use]
138    pub const fn as_str(self) -> &'static str {
139        match self {
140            Self::Text => "bun.lock",
141            Self::Binary => "bun.lockb",
142        }
143    }
144}
145
146fn parse_bun_version(input: &str) -> Result<BunVersion, BunVersionParseError> {
147    let trimmed = input.trim().trim_start_matches('v');
148    if trimmed.is_empty() {
149        return Err(BunVersionParseError::Empty);
150    }
151    let parts = trimmed.split('.').collect::<Vec<_>>();
152    if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
153        return Err(BunVersionParseError::InvalidVersion);
154    }
155    let major = parse_part(parts[0])?;
156    let minor = parts.get(1).copied().map(parse_part).transpose()?;
157    let patch = parts.get(2).copied().map(parse_part).transpose()?;
158    BunVersion::new(major, minor, patch)
159}
160
161fn parse_part(input: &str) -> Result<u16, BunVersionParseError> {
162    input
163        .parse::<u16>()
164        .map_err(|_error| BunVersionParseError::InvalidVersion)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::{BunCommand, BunLockfile, BunVersion};
170
171    #[test]
172    fn parses_bun_metadata() -> Result<(), Box<dyn std::error::Error>> {
173        let version: BunVersion = "1.1.8".parse()?;
174        assert_eq!(version.major(), 1);
175        assert_eq!("install".parse::<BunCommand>()?, BunCommand::Install);
176        assert_eq!(BunLockfile::Binary.as_str(), "bun.lockb");
177        Ok(())
178    }
179}