1use std::{fmt, ops::Deref, str::FromStr};
16
17use miette::IntoDiagnostic;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug)]
22#[serde(try_from = "String", into = "String")]
23pub struct PackageName(String);
24
25#[derive(thiserror::Error, Debug, PartialEq)]
27pub enum PackageNameError {
28 #[error("package name must be at least one character long, but was empty")]
30 Empty,
31 #[error("package names must be at most 128 characters long, but was {0:}")]
33 TooLong(usize),
34 #[error("package name must start with alphabetic character, but was {0:}")]
36 InvalidStart(char),
37 #[error(
39 "package name must consist of only ASCII lowercase and dashes, but contains {0:} at position {1:}"
40 )]
41 InvalidCharacter(char, usize),
42}
43
44impl PackageName {
45 const MIN_LENGTH: usize = 1;
46 const MAX_LENGTH: usize = 128;
47
48 pub fn new<S: Into<String>>(value: S) -> Result<Self, PackageNameError> {
50 let value = value.into();
51 Self::validate(&value)?;
52 Ok(Self(value))
53 }
54
55 pub(crate) fn unchecked<S: Into<String>>(value: S) -> Self {
57 Self(value.into())
58 }
59
60 fn is_allowed_start(c: char) -> bool {
62 c.is_alphabetic()
63 }
64
65 fn is_allowed(c: char) -> bool {
67 let is_ascii_lowercase_alphanumeric =
68 |c: char| c.is_ascii_alphanumeric() && !c.is_ascii_uppercase();
69 match c {
70 '-' => true,
71 c if is_ascii_lowercase_alphanumeric(c) => true,
72 _ => false,
73 }
74 }
75
76 pub fn validate(name: impl AsRef<str>) -> Result<(), PackageNameError> {
78 let name = name.as_ref();
79
80 if name.len() < Self::MIN_LENGTH {
82 return Err(PackageNameError::Empty);
83 }
84
85 if name.len() > Self::MAX_LENGTH {
86 return Err(PackageNameError::TooLong(name.len()));
87 }
88
89 match name.chars().next() {
91 Some(c) if Self::is_allowed_start(c) => {}
92 Some(c) => return Err(PackageNameError::InvalidStart(c)),
93 None => unreachable!(),
94 }
95
96 let illegal = name
98 .chars()
99 .enumerate()
100 .find(|(_, c)| !Self::is_allowed(*c));
101
102 if let Some((index, c)) = illegal {
103 return Err(PackageNameError::InvalidCharacter(c, index));
104 }
105
106 Ok(())
107 }
108}
109
110impl TryFrom<String> for PackageName {
111 type Error = PackageNameError;
112
113 fn try_from(value: String) -> Result<Self, Self::Error> {
114 Self::new(value)
115 }
116}
117
118impl FromStr for PackageName {
119 type Err = miette::Report;
120
121 fn from_str(input: &str) -> Result<Self, Self::Err> {
122 Self::new(input).into_diagnostic()
123 }
124}
125
126impl From<PackageName> for String {
127 fn from(s: PackageName) -> Self {
128 s.to_string()
129 }
130}
131
132impl Deref for PackageName {
133 type Target = str;
134
135 fn deref(&self) -> &Self::Target {
136 &self.0
137 }
138}
139
140impl fmt::Display for PackageName {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 self.0.fmt(f)
143 }
144}
145
146#[cfg(test)]
147mod test {
148 use super::*;
149
150 #[test]
151 fn ascii_lowercase() {
152 assert_eq!(PackageName::new("abc"), Ok(PackageName("abc".into())));
153 assert_eq!(PackageName::new("abc"), Ok(PackageName("abc".into())));
154 }
155
156 #[test]
157 fn short() {
158 assert_eq!(PackageName::new("a"), Ok(PackageName("a".into())));
159 assert_eq!(PackageName::new("ab"), Ok(PackageName("ab".into())));
160 }
161
162 #[test]
163 fn long() {
164 assert_eq!(
165 PackageName::new("a".repeat(128)),
166 Ok(PackageName("a".repeat(128)))
167 );
168
169 assert_eq!(
170 PackageName::new("a".repeat(129)),
171 Err(PackageNameError::TooLong(129))
172 );
173 }
174
175 #[test]
176 fn empty() {
177 assert_eq!(PackageName::new(""), Err(PackageNameError::Empty));
178 }
179
180 #[test]
181 fn numeric_start() {
182 assert_eq!(
183 PackageName::new("4abc"),
184 Err(PackageNameError::InvalidStart('4'))
185 );
186 }
187
188 #[test]
189 fn snake_case() {
190 assert_eq!(
191 PackageName::new("with_underscore"),
192 Err(PackageNameError::InvalidCharacter('_', 4))
193 );
194 }
195}