uv_normalize/
group_name.rs

1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::sync::LazyLock;
6
7use serde::ser::SerializeSeq;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use uv_small_str::SmallString;
11
12use crate::{
13    InvalidNameError, InvalidPipGroupError, InvalidPipGroupPathError, validate_and_normalize_ref,
14};
15
16/// The normalized name of a dependency group.
17///
18/// See:
19/// - <https://peps.python.org/pep-0735/>
20/// - <https://packaging.python.org/en/latest/specifications/name-normalization/>
21#[derive(
22    Debug,
23    Clone,
24    PartialEq,
25    Eq,
26    PartialOrd,
27    Ord,
28    Hash,
29    rkyv::Archive,
30    rkyv::Deserialize,
31    rkyv::Serialize,
32)]
33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34#[rkyv(derive(Debug))]
35pub struct GroupName(SmallString);
36
37impl GroupName {
38    /// Create a validated, normalized group name.
39    ///
40    /// At present, this is no more efficient than calling [`GroupName::from_str`].
41    #[allow(clippy::needless_pass_by_value)]
42    pub fn from_owned(name: String) -> Result<Self, InvalidNameError> {
43        validate_and_normalize_ref(&name).map(Self)
44    }
45
46    /// Return the underlying group name as a string.
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50}
51
52impl FromStr for GroupName {
53    type Err = InvalidNameError;
54
55    fn from_str(name: &str) -> Result<Self, Self::Err> {
56        validate_and_normalize_ref(name).map(Self)
57    }
58}
59
60impl<'de> Deserialize<'de> for GroupName {
61    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62    where
63        D: Deserializer<'de>,
64    {
65        struct Visitor;
66
67        impl serde::de::Visitor<'_> for Visitor {
68            type Value = GroupName;
69
70            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
71                f.write_str("a string")
72            }
73
74            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
75                GroupName::from_str(v).map_err(serde::de::Error::custom)
76            }
77
78            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
79                GroupName::from_owned(v).map_err(serde::de::Error::custom)
80            }
81        }
82
83        deserializer.deserialize_str(Visitor)
84    }
85}
86
87impl Serialize for GroupName {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        self.0.serialize(serializer)
93    }
94}
95
96impl std::fmt::Display for GroupName {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        self.0.fmt(f)
99    }
100}
101
102impl AsRef<str> for GroupName {
103    fn as_ref(&self) -> &str {
104        &self.0
105    }
106}
107
108/// The pip-compatible variant of a [`GroupName`].
109///
110/// Either <groupname> or <path>:<groupname>.
111/// If <path> is omitted it defaults to "pyproject.toml".
112#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
113#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
114pub struct PipGroupName {
115    pub path: Option<PathBuf>,
116    pub name: GroupName,
117}
118
119impl FromStr for PipGroupName {
120    type Err = InvalidPipGroupError;
121
122    fn from_str(path_and_name: &str) -> Result<Self, Self::Err> {
123        // The syntax is `<path>:<name>`.
124        //
125        // `:` isn't valid as part of a dependency-group name, but it can appear in a path.
126        // Therefore we look for the first `:` starting from the end to find the delimiter.
127        // If there is no `:` then there's no path and we use the default one.
128        if let Some((path, name)) = path_and_name.rsplit_once(':') {
129            // pip hard errors if the path does not end with pyproject.toml
130            if !path.ends_with("pyproject.toml") {
131                Err(InvalidPipGroupPathError(path.to_owned()))?;
132            }
133
134            let name = GroupName::from_str(name)?;
135            let path = Some(PathBuf::from(path));
136            Ok(Self { path, name })
137        } else {
138            let name = GroupName::from_str(path_and_name)?;
139            let path = None;
140            Ok(Self { path, name })
141        }
142    }
143}
144
145impl<'de> Deserialize<'de> for PipGroupName {
146    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147    where
148        D: Deserializer<'de>,
149    {
150        let s = <Cow<'_, str>>::deserialize(deserializer)?;
151        Self::from_str(&s).map_err(serde::de::Error::custom)
152    }
153}
154
155impl Serialize for PipGroupName {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: Serializer,
159    {
160        let string = self.to_string();
161        string.serialize(serializer)
162    }
163}
164
165impl Display for PipGroupName {
166    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
167        if let Some(path) = &self.path {
168            write!(f, "{}:{}", path.display(), self.name)
169        } else {
170            self.name.fmt(f)
171        }
172    }
173}
174
175/// Either the literal "all" or a list of groups
176#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
177pub enum DefaultGroups {
178    /// All groups are defaulted
179    All,
180    /// A list of groups
181    List(Vec<GroupName>),
182}
183
184#[cfg(feature = "schemars")]
185impl schemars::JsonSchema for DefaultGroups {
186    fn schema_name() -> Cow<'static, str> {
187        Cow::Borrowed("DefaultGroups")
188    }
189
190    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
191        schemars::json_schema!({
192            "description": "Either the literal \"all\" or a list of groups",
193            "oneOf": [
194                {
195                    "description": "All groups are defaulted",
196                    "type": "string",
197                    "const": "all"
198                },
199                {
200                    "description": "A list of groups",
201                    "type": "array",
202                    "items": generator.subschema_for::<GroupName>()
203                }
204            ]
205        })
206    }
207}
208
209/// Serialize a [`DefaultGroups`] struct into a list of marker strings.
210impl serde::Serialize for DefaultGroups {
211    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212    where
213        S: serde::Serializer,
214    {
215        match self {
216            Self::All => serializer.serialize_str("all"),
217            Self::List(groups) => {
218                let mut seq = serializer.serialize_seq(Some(groups.len()))?;
219                for group in groups {
220                    seq.serialize_element(&group)?;
221                }
222                seq.end()
223            }
224        }
225    }
226}
227
228/// Deserialize a "all" or list of [`GroupName`] into a [`DefaultGroups`] enum.
229impl<'de> serde::Deserialize<'de> for DefaultGroups {
230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231    where
232        D: serde::Deserializer<'de>,
233    {
234        struct StringOrVecVisitor;
235
236        impl<'de> serde::de::Visitor<'de> for StringOrVecVisitor {
237            type Value = DefaultGroups;
238
239            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
240                formatter.write_str(r#"the string "all" or a list of strings"#)
241            }
242
243            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
244            where
245                E: serde::de::Error,
246            {
247                if value != "all" {
248                    return Err(serde::de::Error::custom(
249                        r#"default-groups must be "all" or a ["list", "of", "groups"]"#,
250                    ));
251                }
252                Ok(DefaultGroups::All)
253            }
254
255            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
256            where
257                A: serde::de::SeqAccess<'de>,
258            {
259                let mut groups = Vec::new();
260
261                while let Some(elem) = seq.next_element::<GroupName>()? {
262                    groups.push(elem);
263                }
264
265                Ok(DefaultGroups::List(groups))
266            }
267        }
268
269        deserializer.deserialize_any(StringOrVecVisitor)
270    }
271}
272
273impl Default for DefaultGroups {
274    /// Note this is an "empty" default unlike other contexts where `["dev"]` is the default
275    fn default() -> Self {
276        Self::List(Vec::new())
277    }
278}
279
280/// The name of the global `dev-dependencies` group.
281///
282/// Internally, we model dependency groups as a generic concept; but externally, we only expose the
283/// `dev-dependencies` group.
284pub static DEV_DEPENDENCIES: LazyLock<GroupName> =
285    LazyLock::new(|| GroupName::from_str("dev").unwrap());