Skip to main content

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    #[expect(clippy::needless_pass_by_value)]
42    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
184impl DefaultGroups {
185    /// Create a sorted, deduplicated list of default dependency groups.
186    pub fn from_groups(mut groups: Vec<GroupName>) -> Self {
187        groups.sort_unstable();
188        groups.dedup();
189        Self::List(groups)
190    }
191}
192
193#[cfg(feature = "schemars")]
194impl schemars::JsonSchema for DefaultGroups {
195    fn schema_name() -> Cow<'static, str> {
196        Cow::Borrowed("DefaultGroups")
197    }
198
199    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
200        schemars::json_schema!({
201            "description": "Either the literal \"all\" or a list of groups",
202            "oneOf": [
203                {
204                    "description": "All groups are defaulted",
205                    "type": "string",
206                    "const": "all"
207                },
208                {
209                    "description": "A list of groups",
210                    "type": "array",
211                    "items": generator.subschema_for::<GroupName>()
212                }
213            ]
214        })
215    }
216}
217
218/// Serialize a [`DefaultGroups`] struct into a list of marker strings.
219impl serde::Serialize for DefaultGroups {
220    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
221    where
222        S: serde::Serializer,
223    {
224        match self {
225            Self::All => serializer.serialize_str("all"),
226            Self::List(groups) => {
227                let mut seq = serializer.serialize_seq(Some(groups.len()))?;
228                for group in groups {
229                    seq.serialize_element(&group)?;
230                }
231                seq.end()
232            }
233        }
234    }
235}
236
237/// Deserialize a "all" or list of [`GroupName`] into a [`DefaultGroups`] enum.
238impl<'de> serde::Deserialize<'de> for DefaultGroups {
239    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240    where
241        D: serde::Deserializer<'de>,
242    {
243        struct StringOrVecVisitor;
244
245        impl<'de> serde::de::Visitor<'de> for StringOrVecVisitor {
246            type Value = DefaultGroups;
247
248            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
249                formatter.write_str(r#"the string "all" or a list of strings"#)
250            }
251
252            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
253            where
254                E: serde::de::Error,
255            {
256                if value != "all" {
257                    return Err(serde::de::Error::custom(
258                        r#"default-groups must be "all" or a ["list", "of", "groups"]"#,
259                    ));
260                }
261                Ok(DefaultGroups::All)
262            }
263
264            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
265            where
266                A: serde::de::SeqAccess<'de>,
267            {
268                let mut groups = Vec::new();
269
270                while let Some(elem) = seq.next_element::<GroupName>()? {
271                    groups.push(elem);
272                }
273
274                Ok(DefaultGroups::from_groups(groups))
275            }
276        }
277
278        deserializer.deserialize_any(StringOrVecVisitor)
279    }
280}
281
282impl Default for DefaultGroups {
283    /// Note this is an "empty" default unlike other contexts where `["dev"]` is the default
284    fn default() -> Self {
285        Self::List(Vec::new())
286    }
287}
288
289/// The name of the global `dev-dependencies` group.
290///
291/// Internally, we model dependency groups as a generic concept; but externally, we only expose the
292/// `dev-dependencies` group.
293pub static DEV_DEPENDENCIES: LazyLock<GroupName> =
294    LazyLock::new(|| GroupName::from_str("dev").unwrap());