use serde::{Deserialize, Serialize};
use thiserror::Error;
const RESERVED_NAMES: &[&str] = &["test", "proc-macro", "proc_macro", "build"];
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CrateName(String);
impl CrateName {
pub fn new(name: impl Into<String>) -> Result<Self, InvalidCrateNameError> {
let name = name.into();
Self::validate(&name)?;
Ok(Self(name))
}
pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
Self(name.into())
}
#[cfg(any(test, feature = "test-utils"))]
pub fn new_for_test(name: impl Into<String>) -> Self {
Self(name.into())
}
fn validate(name: &str) -> Result<(), InvalidCrateNameError> {
if name.is_empty() {
return Err(InvalidCrateNameError::Empty);
}
if name.len() > 64 {
return Err(InvalidCrateNameError::TooLong(name.to_string()));
}
if RESERVED_NAMES.contains(&name) {
return Err(InvalidCrateNameError::Reserved(name.to_string()));
}
let mut chars = name.chars();
let first = chars
.next()
.expect("non-empty checked at function entry (name.is_empty() guard)");
if !first.is_ascii_alphabetic() {
return Err(InvalidCrateNameError::InvalidStart(name.to_string()));
}
for c in chars {
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
return Err(InvalidCrateNameError::InvalidChar(name.to_string()));
}
}
Ok(())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_module_name(&self) -> String {
self.0.replace('-', "_")
}
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for CrateName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CrateName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Error)]
pub enum InvalidCrateNameError {
#[error("crate name cannot be empty")]
Empty,
#[error("crate name must start with a letter: '{0}'")]
InvalidStart(String),
#[error("crate name contains invalid character: '{0}'")]
InvalidChar(String),
#[error("crate name too long (max 64 chars): '{0}'")]
TooLong(String),
#[error("crate name is reserved: '{0}'")]
Reserved(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_crate_names() {
assert!(CrateName::new("ryo").is_ok());
assert!(CrateName::new("ryo-mutations").is_ok());
assert!(CrateName::new("ryo_symbol").is_ok());
assert!(CrateName::new("Tokio").is_ok());
assert!(CrateName::new("a").is_ok());
}
#[test]
fn test_invalid_crate_names() {
assert!(matches!(
CrateName::new(""),
Err(InvalidCrateNameError::Empty)
));
assert!(matches!(
CrateName::new("123abc"),
Err(InvalidCrateNameError::InvalidStart(_))
));
assert!(matches!(
CrateName::new("-abc"),
Err(InvalidCrateNameError::InvalidStart(_))
));
assert!(matches!(
CrateName::new("abc.def"),
Err(InvalidCrateNameError::InvalidChar(_))
));
assert!(matches!(
CrateName::new("test"),
Err(InvalidCrateNameError::Reserved(_))
));
}
#[test]
fn test_to_module_name() {
let name = CrateName::new("ryo-mutations").unwrap();
assert_eq!(name.to_module_name(), "ryo_mutations");
let name = CrateName::new("already_underscore").unwrap();
assert_eq!(name.to_module_name(), "already_underscore");
}
}