Skip to main content

uv_pypi_types/
module_name.rs

1use std::borrow::Cow;
2use std::fmt::Display;
3use std::str::FromStr;
4
5use serde::{Serialize, Serializer};
6use thiserror::Error;
7
8use crate::{Identifier, IdentifierParseError};
9
10/// The name of an importable Python module.
11///
12/// This is a dotted sequence of Python identifiers, like `foo` or `foo.bar`.
13#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
14pub struct ModuleName(Box<str>);
15
16#[derive(Debug, Clone, Error)]
17pub enum ModuleNameParseError {
18    #[error("A module name must not be empty")]
19    Empty,
20    #[error("Invalid module name component `{component}` in `{module}`")]
21    InvalidComponent {
22        component: Box<str>,
23        module: Box<str>,
24        #[source]
25        err: IdentifierParseError,
26    },
27}
28
29impl ModuleName {
30    pub fn new(module: impl Into<Box<str>>) -> Result<Self, ModuleNameParseError> {
31        let module = module.into();
32        if module.is_empty() {
33            return Err(ModuleNameParseError::Empty);
34        }
35
36        for component in module.split('.') {
37            Self::validate_component(&module, component)?;
38        }
39
40        Ok(Self(module))
41    }
42
43    pub fn from_components<'a>(
44        components: impl IntoIterator<Item = &'a str>,
45    ) -> Result<Self, ModuleNameParseError> {
46        let components = components.into_iter().collect::<Vec<_>>();
47        if components.is_empty() {
48            return Err(ModuleNameParseError::Empty);
49        }
50
51        let module = components.join(".").into_boxed_str();
52        for component in components {
53            Self::validate_component(&module, component)?;
54        }
55
56        Ok(Self(module))
57    }
58
59    /// Iterate over this module and its parent modules.
60    ///
61    /// For example, `foo.bar.baz` yields `foo`, `foo.bar`, and `foo.bar.baz`.
62    pub fn prefixes(&self) -> impl Iterator<Item = Self> + '_ {
63        self.0
64            .match_indices('.')
65            .map(|(index, _)| Self(Box::from(&self.0[..index])))
66            .chain(std::iter::once(self.clone()))
67    }
68
69    fn validate_component(module: &str, component: &str) -> Result<(), ModuleNameParseError> {
70        Identifier::new(component.to_string()).map_err(|err| {
71            ModuleNameParseError::InvalidComponent {
72                component: component.to_string().into_boxed_str(),
73                module: module.into(),
74                err,
75            }
76        })?;
77        Ok(())
78    }
79}
80
81impl FromStr for ModuleName {
82    type Err = ModuleNameParseError;
83
84    fn from_str(module: &str) -> Result<Self, Self::Err> {
85        Self::new(module)
86    }
87}
88
89impl Display for ModuleName {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{}", self.0)
92    }
93}
94
95impl AsRef<str> for ModuleName {
96    fn as_ref(&self) -> &str {
97        &self.0
98    }
99}
100
101impl<'de> serde::de::Deserialize<'de> for ModuleName {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: serde::de::Deserializer<'de>,
105    {
106        let s = <Cow<'_, str>>::deserialize(deserializer)?;
107        Self::from_str(&s).map_err(serde::de::Error::custom)
108    }
109}
110
111impl Serialize for ModuleName {
112    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113    where
114        S: Serializer,
115    {
116        Serialize::serialize(&self.0, serializer)
117    }
118}
119
120#[cfg(feature = "schemars")]
121impl schemars::JsonSchema for ModuleName {
122    fn schema_name() -> Cow<'static, str> {
123        Cow::Borrowed("ModuleName")
124    }
125
126    fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
127        schemars::json_schema!({
128            "type": "string",
129            "pattern": r"^[_\p{Alphabetic}][_0-9\p{Alphabetic}]*(\.[_\p{Alphabetic}][_0-9\p{Alphabetic}]*)*$",
130            "description": "A dotted Python module name"
131        })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::str::FromStr;
138
139    use insta::assert_snapshot;
140
141    use super::ModuleName;
142
143    #[test]
144    fn valid() {
145        for module_name in ["abc", "abc.def", "_abc", "férrîs", "package.안녕하세요"] {
146            assert!(ModuleName::from_str(module_name).is_ok(), "{module_name}");
147        }
148    }
149
150    #[test]
151    fn invalid() {
152        assert_snapshot!(
153            ModuleName::from_str("foo-bar").unwrap_err(),
154            @"Invalid module name component `foo-bar` in `foo-bar`"
155        );
156        assert_snapshot!(
157            ModuleName::from_str("foo.").unwrap_err(),
158            @"Invalid module name component `` in `foo.`"
159        );
160    }
161
162    #[test]
163    fn prefixes() {
164        let prefixes = ModuleName::from_str("foo.bar.baz")
165            .expect("valid module name")
166            .prefixes()
167            .map(|module| module.to_string())
168            .collect::<Vec<_>>();
169
170        assert_eq!(prefixes, ["foo", "foo.bar", "foo.bar.baz"]);
171    }
172}