uv_pypi_types/
module_name.rs1use 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#[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 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}