uv_normalize/
package_name.rs

1use std::borrow::Cow;
2use std::cmp::PartialEq;
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize};
6
7use uv_small_str::SmallString;
8
9use crate::{InvalidNameError, validate_and_normalize_ref};
10
11/// The normalized name of a package.
12///
13/// Converts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.
14/// For example, `---`, `.`, and `__` are all converted to a single `-`.
15///
16/// See: <https://packaging.python.org/en/latest/specifications/name-normalization/>
17#[derive(
18    Debug,
19    Clone,
20    PartialEq,
21    Eq,
22    PartialOrd,
23    Ord,
24    Hash,
25    Serialize,
26    rkyv::Archive,
27    rkyv::Deserialize,
28    rkyv::Serialize,
29)]
30#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
31#[rkyv(derive(Debug))]
32pub struct PackageName(SmallString);
33
34impl PackageName {
35    /// Create a validated, normalized package name.
36    ///
37    /// At present, this is no more efficient than calling [`PackageName::from_str`].
38    #[allow(clippy::needless_pass_by_value)]
39    pub fn from_owned(name: String) -> Result<Self, InvalidNameError> {
40        validate_and_normalize_ref(&name).map(Self)
41    }
42
43    /// Escape this name with underscores (`_`) instead of dashes (`-`)
44    ///
45    /// See: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
46    pub fn as_dist_info_name(&self) -> Cow<'_, str> {
47        if let Some(dash_position) = self.0.find('-') {
48            // Initialize `replaced` with the start of the string up to the current character.
49            let mut owned_string = String::with_capacity(self.0.len());
50            owned_string.push_str(&self.0[..dash_position]);
51            owned_string.push('_');
52
53            // Iterate over the rest of the string.
54            owned_string.extend(
55                self.0[dash_position + 1..]
56                    .chars()
57                    .map(|character| if character == '-' { '_' } else { character }),
58            );
59
60            Cow::Owned(owned_string)
61        } else {
62            Cow::Borrowed(self.0.as_ref())
63        }
64    }
65
66    /// Returns the underlying package name.
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70}
71
72impl From<&Self> for PackageName {
73    /// Required for `WaitMap::wait`.
74    fn from(package_name: &Self) -> Self {
75        package_name.clone()
76    }
77}
78
79impl FromStr for PackageName {
80    type Err = InvalidNameError;
81
82    fn from_str(name: &str) -> Result<Self, Self::Err> {
83        validate_and_normalize_ref(name).map(Self)
84    }
85}
86
87impl<'de> Deserialize<'de> for PackageName {
88    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89    where
90        D: Deserializer<'de>,
91    {
92        struct Visitor;
93
94        impl serde::de::Visitor<'_> for Visitor {
95            type Value = PackageName;
96
97            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
98                f.write_str("a string")
99            }
100
101            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
102                PackageName::from_str(v).map_err(serde::de::Error::custom)
103            }
104
105            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
106                PackageName::from_owned(v).map_err(serde::de::Error::custom)
107            }
108        }
109
110        deserializer.deserialize_str(Visitor)
111    }
112}
113
114impl std::fmt::Display for PackageName {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        self.0.fmt(f)
117    }
118}
119
120impl AsRef<str> for PackageName {
121    fn as_ref(&self) -> &str {
122        &self.0
123    }
124}