Skip to main content

use_uv/
lib.rs

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            /// Creates non-empty uv metadata text.
14            ///
15            /// # Errors
16            ///
17            /// Returns [`UvTextError::Empty`] when `input` is empty after trimming.
18            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            /// Returns the stored text.
28            #[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/// Common uv command labels.
59#[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    /// Returns the command label.
77    #[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/// uv project subcommand labels.
125#[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    /// Returns the subcommand label.
139    #[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/// uv python subcommand labels.
179#[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    /// Returns the subcommand label.
189    #[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/// uv tool subcommand labels.
221#[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    /// Returns the subcommand label.
232    #[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/// uv lockfile labels.
266#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
267pub enum UvLockfile {
268    UvLock,
269}
270
271impl UvLockfile {
272    /// Returns the lockfile label.
273    #[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/// uv config file labels.
298#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
299pub enum UvConfigFile {
300    PyProjectToml,
301    UvToml,
302}
303
304impl UvConfigFile {
305    /// Returns the config file label.
306    #[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/// Error returned when uv metadata text is invalid.
338#[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}