1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6const RESERVED_NAMES: &[&str] = &["test", "proc-macro", "proc_macro", "build"];
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct CrateName(String);
28
29impl CrateName {
30 pub fn new(name: impl Into<String>) -> Result<Self, InvalidCrateNameError> {
39 let name = name.into();
40 Self::validate(&name)?;
41 Ok(Self(name))
42 }
43
44 pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
46 Self(name.into())
47 }
48
49 #[cfg(any(test, feature = "test-utils"))]
51 pub fn new_for_test(name: impl Into<String>) -> Self {
52 Self(name.into())
53 }
54
55 fn validate(name: &str) -> Result<(), InvalidCrateNameError> {
56 if name.is_empty() {
58 return Err(InvalidCrateNameError::Empty);
59 }
60
61 if name.len() > 64 {
63 return Err(InvalidCrateNameError::TooLong(name.to_string()));
64 }
65
66 if RESERVED_NAMES.contains(&name) {
68 return Err(InvalidCrateNameError::Reserved(name.to_string()));
69 }
70
71 let mut chars = name.chars();
73 let first = chars
74 .next()
75 .expect("non-empty checked at function entry (name.is_empty() guard)");
76 if !first.is_ascii_alphabetic() {
77 return Err(InvalidCrateNameError::InvalidStart(name.to_string()));
78 }
79
80 for c in chars {
82 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
83 return Err(InvalidCrateNameError::InvalidChar(name.to_string()));
84 }
85 }
86
87 Ok(())
88 }
89
90 pub fn as_str(&self) -> &str {
92 &self.0
93 }
94
95 pub fn to_module_name(&self) -> String {
104 self.0.replace('-', "_")
105 }
106
107 pub fn into_string(self) -> String {
109 self.0
110 }
111}
112
113impl AsRef<str> for CrateName {
114 fn as_ref(&self) -> &str {
115 &self.0
116 }
117}
118
119impl std::fmt::Display for CrateName {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "{}", self.0)
122 }
123}
124
125#[derive(Debug, Clone, Error)]
127pub enum InvalidCrateNameError {
128 #[error("crate name cannot be empty")]
130 Empty,
131
132 #[error("crate name must start with a letter: '{0}'")]
135 InvalidStart(String),
136
137 #[error("crate name contains invalid character: '{0}'")]
140 InvalidChar(String),
141
142 #[error("crate name too long (max 64 chars): '{0}'")]
146 TooLong(String),
147
148 #[error("crate name is reserved: '{0}'")]
151 Reserved(String),
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_valid_crate_names() {
160 assert!(CrateName::new("ryo").is_ok());
161 assert!(CrateName::new("ryo-mutations").is_ok());
162 assert!(CrateName::new("ryo_symbol").is_ok());
163 assert!(CrateName::new("Tokio").is_ok());
164 assert!(CrateName::new("a").is_ok());
165 }
166
167 #[test]
168 fn test_invalid_crate_names() {
169 assert!(matches!(
171 CrateName::new(""),
172 Err(InvalidCrateNameError::Empty)
173 ));
174
175 assert!(matches!(
177 CrateName::new("123abc"),
178 Err(InvalidCrateNameError::InvalidStart(_))
179 ));
180
181 assert!(matches!(
183 CrateName::new("-abc"),
184 Err(InvalidCrateNameError::InvalidStart(_))
185 ));
186
187 assert!(matches!(
189 CrateName::new("abc.def"),
190 Err(InvalidCrateNameError::InvalidChar(_))
191 ));
192
193 assert!(matches!(
195 CrateName::new("test"),
196 Err(InvalidCrateNameError::Reserved(_))
197 ));
198 }
199
200 #[test]
201 fn test_to_module_name() {
202 let name = CrateName::new("ryo-mutations").unwrap();
203 assert_eq!(name.to_module_name(), "ryo_mutations");
204
205 let name = CrateName::new("already_underscore").unwrap();
206 assert_eq!(name.to_module_name(), "already_underscore");
207 }
208}