1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! uv_text_newtype {
8 ($name:ident) => {
9 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10 pub struct $name(String);
11
12 impl $name {
13 pub fn new(input: &str) -> Result<Self, UvTextError> {
19 let trimmed = input.trim();
20 if trimmed.is_empty() {
21 Err(UvTextError::Empty)
22 } else {
23 Ok(Self(trimmed.to_string()))
24 }
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32 }
33
34 impl fmt::Display for $name {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter.write_str(self.as_str())
37 }
38 }
39
40 impl FromStr for $name {
41 type Err = UvTextError;
42
43 fn from_str(input: &str) -> Result<Self, Self::Err> {
44 Self::new(input)
45 }
46 }
47
48 impl TryFrom<&str> for $name {
49 type Error = UvTextError;
50
51 fn try_from(value: &str) -> Result<Self, Self::Error> {
52 Self::new(value)
53 }
54 }
55 };
56}
57
58#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub enum UvCommand {
61 Init,
62 Add,
63 Remove,
64 Sync,
65 Lock,
66 Run,
67 Build,
68 Publish,
69 Python,
70 Pip,
71 Tool,
72 Venv,
73}
74
75impl UvCommand {
76 #[must_use]
78 pub const fn as_str(self) -> &'static str {
79 match self {
80 Self::Init => "init",
81 Self::Add => "add",
82 Self::Remove => "remove",
83 Self::Sync => "sync",
84 Self::Lock => "lock",
85 Self::Run => "run",
86 Self::Build => "build",
87 Self::Publish => "publish",
88 Self::Python => "python",
89 Self::Pip => "pip",
90 Self::Tool => "tool",
91 Self::Venv => "venv",
92 }
93 }
94}
95
96impl fmt::Display for UvCommand {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str(self.as_str())
99 }
100}
101
102impl FromStr for UvCommand {
103 type Err = UvTextError;
104
105 fn from_str(input: &str) -> Result<Self, Self::Err> {
106 match normalized(input)?.as_str() {
107 "init" => Ok(Self::Init),
108 "add" => Ok(Self::Add),
109 "remove" => Ok(Self::Remove),
110 "sync" => Ok(Self::Sync),
111 "lock" => Ok(Self::Lock),
112 "run" => Ok(Self::Run),
113 "build" => Ok(Self::Build),
114 "publish" => Ok(Self::Publish),
115 "python" => Ok(Self::Python),
116 "pip" => Ok(Self::Pip),
117 "tool" => Ok(Self::Tool),
118 "venv" => Ok(Self::Venv),
119 _ => Err(UvTextError::Unknown),
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub enum UvProjectCommand {
127 Init,
128 Add,
129 Remove,
130 Sync,
131 Lock,
132 Run,
133 Build,
134 Publish,
135}
136
137impl UvProjectCommand {
138 #[must_use]
140 pub const fn as_str(self) -> &'static str {
141 match self {
142 Self::Init => "init",
143 Self::Add => "add",
144 Self::Remove => "remove",
145 Self::Sync => "sync",
146 Self::Lock => "lock",
147 Self::Run => "run",
148 Self::Build => "build",
149 Self::Publish => "publish",
150 }
151 }
152}
153
154impl fmt::Display for UvProjectCommand {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 formatter.write_str(self.as_str())
157 }
158}
159
160impl FromStr for UvProjectCommand {
161 type Err = UvTextError;
162
163 fn from_str(input: &str) -> Result<Self, Self::Err> {
164 match normalized(input)?.as_str() {
165 "init" => Ok(Self::Init),
166 "add" => Ok(Self::Add),
167 "remove" => Ok(Self::Remove),
168 "sync" => Ok(Self::Sync),
169 "lock" => Ok(Self::Lock),
170 "run" => Ok(Self::Run),
171 "build" => Ok(Self::Build),
172 "publish" => Ok(Self::Publish),
173 _ => Err(UvTextError::Unknown),
174 }
175 }
176}
177
178#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum UvPythonCommand {
181 Install,
182 List,
183 Pin,
184 Dir,
185}
186
187impl UvPythonCommand {
188 #[must_use]
190 pub const fn as_str(self) -> &'static str {
191 match self {
192 Self::Install => "install",
193 Self::List => "list",
194 Self::Pin => "pin",
195 Self::Dir => "dir",
196 }
197 }
198}
199
200impl fmt::Display for UvPythonCommand {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 formatter.write_str(self.as_str())
203 }
204}
205
206impl FromStr for UvPythonCommand {
207 type Err = UvTextError;
208
209 fn from_str(input: &str) -> Result<Self, Self::Err> {
210 match normalized(input)?.as_str() {
211 "install" => Ok(Self::Install),
212 "list" => Ok(Self::List),
213 "pin" => Ok(Self::Pin),
214 "dir" => Ok(Self::Dir),
215 _ => Err(UvTextError::Unknown),
216 }
217 }
218}
219
220#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
222pub enum UvToolCommand {
223 Install,
224 Run,
225 List,
226 Uninstall,
227 Upgrade,
228}
229
230impl UvToolCommand {
231 #[must_use]
233 pub const fn as_str(self) -> &'static str {
234 match self {
235 Self::Install => "install",
236 Self::Run => "run",
237 Self::List => "list",
238 Self::Uninstall => "uninstall",
239 Self::Upgrade => "upgrade",
240 }
241 }
242}
243
244impl fmt::Display for UvToolCommand {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 formatter.write_str(self.as_str())
247 }
248}
249
250impl FromStr for UvToolCommand {
251 type Err = UvTextError;
252
253 fn from_str(input: &str) -> Result<Self, Self::Err> {
254 match normalized(input)?.as_str() {
255 "install" => Ok(Self::Install),
256 "run" => Ok(Self::Run),
257 "list" => Ok(Self::List),
258 "uninstall" => Ok(Self::Uninstall),
259 "upgrade" => Ok(Self::Upgrade),
260 _ => Err(UvTextError::Unknown),
261 }
262 }
263}
264
265#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
267pub enum UvLockfile {
268 UvLock,
269}
270
271impl UvLockfile {
272 #[must_use]
274 pub const fn as_str(self) -> &'static str {
275 "uv.lock"
276 }
277}
278
279impl fmt::Display for UvLockfile {
280 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
281 formatter.write_str(self.as_str())
282 }
283}
284
285impl FromStr for UvLockfile {
286 type Err = UvTextError;
287
288 fn from_str(input: &str) -> Result<Self, Self::Err> {
289 match input.trim().to_ascii_lowercase().as_str() {
290 "uv.lock" | "uvlock" => Ok(Self::UvLock),
291 "" => Err(UvTextError::Empty),
292 _ => Err(UvTextError::Unknown),
293 }
294 }
295}
296
297#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
299pub enum UvConfigFile {
300 PyProjectToml,
301 UvToml,
302}
303
304impl UvConfigFile {
305 #[must_use]
307 pub const fn as_str(self) -> &'static str {
308 match self {
309 Self::PyProjectToml => "pyproject.toml",
310 Self::UvToml => "uv.toml",
311 }
312 }
313}
314
315impl fmt::Display for UvConfigFile {
316 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
317 formatter.write_str(self.as_str())
318 }
319}
320
321impl FromStr for UvConfigFile {
322 type Err = UvTextError;
323
324 fn from_str(input: &str) -> Result<Self, Self::Err> {
325 match input.trim().to_ascii_lowercase().as_str() {
326 "pyproject.toml" | "pyprojecttoml" => Ok(Self::PyProjectToml),
327 "uv.toml" | "uvtoml" => Ok(Self::UvToml),
328 "" => Err(UvTextError::Empty),
329 _ => Err(UvTextError::Unknown),
330 }
331 }
332}
333
334uv_text_newtype!(UvWorkspace);
335uv_text_newtype!(UvPackageSpec);
336
337#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub enum UvTextError {
340 Empty,
341 Unknown,
342}
343
344impl fmt::Display for UvTextError {
345 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346 match self {
347 Self::Empty => formatter.write_str("uv metadata text cannot be empty"),
348 Self::Unknown => formatter.write_str("unknown uv command"),
349 }
350 }
351}
352
353impl Error for UvTextError {}
354
355fn normalized(input: &str) -> Result<String, UvTextError> {
356 let trimmed = input.trim();
357 if trimmed.is_empty() {
358 Err(UvTextError::Empty)
359 } else {
360 Ok(trimmed.to_ascii_lowercase())
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::{
367 UvCommand, UvConfigFile, UvLockfile, UvPackageSpec, UvProjectCommand, UvPythonCommand,
368 UvTextError, UvToolCommand, UvWorkspace,
369 };
370
371 #[test]
372 fn models_uv_commands_and_files() -> Result<(), UvTextError> {
373 assert_eq!("sync".parse::<UvCommand>()?, UvCommand::Sync);
374 assert_eq!(
375 "build".parse::<UvProjectCommand>()?,
376 UvProjectCommand::Build
377 );
378 assert_eq!("pin".parse::<UvPythonCommand>()?, UvPythonCommand::Pin);
379 assert_eq!("upgrade".parse::<UvToolCommand>()?, UvToolCommand::Upgrade);
380 assert_eq!(UvLockfile::UvLock.as_str(), "uv.lock");
381 assert_eq!("uv.lock".parse::<UvLockfile>()?.to_string(), "uv.lock");
382 assert_eq!(UvConfigFile::UvToml.as_str(), "uv.toml");
383 assert_eq!(
384 "pyproject.toml".parse::<UvConfigFile>()?,
385 UvConfigFile::PyProjectToml
386 );
387 Ok(())
388 }
389
390 #[test]
391 fn validates_workspace_and_package_specs() -> Result<(), UvTextError> {
392 assert_eq!(UvWorkspace::new("workspace")?.as_str(), "workspace");
393 assert_eq!(UvPackageSpec::new("ruff>=0.4")?.as_str(), "ruff>=0.4");
394 assert_eq!(UvPackageSpec::new(""), Err(UvTextError::Empty));
395 Ok(())
396 }
397}