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 BunVersion {
10 major: u16,
11 minor: Option<u16>,
12 patch: Option<u16>,
13}
14
15impl BunVersion {
16 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 #[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#[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#[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 #[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#[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub enum BunLockfile {
131 Text,
132 Binary,
133}
134
135impl BunLockfile {
136 #[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}