uv_normalize/
group_name.rs

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