use std::{fmt, ops::Deref, str::FromStr};
use miette::IntoDiagnostic;
use serde::{Deserialize, Serialize};
#[derive(Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[serde(try_from = "String", into = "String")]
pub struct PackageName(String);
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum PackageNameError {
#[error("package name must be at least one character long, but was empty")]
Empty,
#[error("package names must be at most 128 characters long, but was {0:}")]
TooLong(usize),
#[error("package name must start with alphabetic character, but was {0:}")]
InvalidStart(char),
#[error(
"package name must consist of only ASCII lowercase and dashes, but contains {0:} at position {1:}"
)]
InvalidCharacter(char, usize),
}
impl PackageName {
const MIN_LENGTH: usize = 1;
const MAX_LENGTH: usize = 128;
pub fn new<S: Into<String>>(value: S) -> Result<Self, PackageNameError> {
let value = value.into();
Self::validate(&value)?;
Ok(Self(value))
}
pub(crate) fn unchecked<S: Into<String>>(value: S) -> Self {
Self(value.into())
}
fn is_allowed_start(c: char) -> bool {
c.is_alphabetic()
}
fn is_allowed(c: char) -> bool {
let is_ascii_lowercase_alphanumeric =
|c: char| c.is_ascii_alphanumeric() && !c.is_ascii_uppercase();
match c {
'-' => true,
c if is_ascii_lowercase_alphanumeric(c) => true,
_ => false,
}
}
pub fn validate(name: impl AsRef<str>) -> Result<(), PackageNameError> {
let name = name.as_ref();
if name.len() < Self::MIN_LENGTH {
return Err(PackageNameError::Empty);
}
if name.len() > Self::MAX_LENGTH {
return Err(PackageNameError::TooLong(name.len()));
}
match name.chars().next() {
Some(c) if Self::is_allowed_start(c) => {}
Some(c) => return Err(PackageNameError::InvalidStart(c)),
None => unreachable!(),
}
let illegal = name
.chars()
.enumerate()
.find(|(_, c)| !Self::is_allowed(*c));
if let Some((index, c)) = illegal {
return Err(PackageNameError::InvalidCharacter(c, index));
}
Ok(())
}
}
impl TryFrom<String> for PackageName {
type Error = PackageNameError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl FromStr for PackageName {
type Err = miette::Report;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input).into_diagnostic()
}
}
impl From<PackageName> for String {
fn from(s: PackageName) -> Self {
s.to_string()
}
}
impl Deref for PackageName {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for PackageName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn ascii_lowercase() {
assert_eq!(PackageName::new("abc"), Ok(PackageName("abc".into())));
assert_eq!(PackageName::new("abc"), Ok(PackageName("abc".into())));
}
#[test]
fn short() {
assert_eq!(PackageName::new("a"), Ok(PackageName("a".into())));
assert_eq!(PackageName::new("ab"), Ok(PackageName("ab".into())));
}
#[test]
fn long() {
assert_eq!(
PackageName::new("a".repeat(128)),
Ok(PackageName("a".repeat(128)))
);
assert_eq!(
PackageName::new("a".repeat(129)),
Err(PackageNameError::TooLong(129))
);
}
#[test]
fn empty() {
assert_eq!(PackageName::new(""), Err(PackageNameError::Empty));
}
#[test]
fn numeric_start() {
assert_eq!(
PackageName::new("4abc"),
Err(PackageNameError::InvalidStart('4'))
);
}
#[test]
fn snake_case() {
assert_eq!(
PackageName::new("with_underscore"),
Err(PackageNameError::InvalidCharacter('_', 4))
);
}
}