use crate::error::Error;
use std::{fmt::Display, str::FromStr};
use tracing::error;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct Name(String);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum NameErrorKind {
Empty,
InvalidInitialChar,
InvalidChar,
}
impl Display for Name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<Name> for String {
fn from(id: Name) -> Self {
id.0
}
}
impl FromStr for Name {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
match chars.next() {
None => {
error!("Name::from_str; value is empty");
Err(NameErrorKind::Empty.into())
}
Some(first) if !first.is_ascii_alphabetic() => {
error!("Name::from_str; initial character must be alphabetic");
Err(NameErrorKind::InvalidInitialChar.into())
}
Some(_) if !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') => {
error!("Name::from_str; character must be alphanumeric, '-', or '_'");
Err(NameErrorKind::InvalidChar.into())
}
Some(_) => Ok(Self(s.to_string())),
}
}
}
impl Display for NameErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NameErrorKind::Empty => write!(f, "Name cannot be empty"),
NameErrorKind::InvalidInitialChar => {
write!(f, "Initial character must be an ASCII alphabetic character")
}
NameErrorKind::InvalidChar => {
write!(f, "Characters must be ASCII alphanumeric, '-', or '_'")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_simple_name() {
let name: Result<Name, _> = "myplugin".parse();
assert!(name.is_ok());
assert_eq!(name.unwrap().to_string(), "myplugin");
}
#[test]
fn valid_name_with_hyphens() {
let name: Result<Name, _> = "my-plugin".parse();
assert!(name.is_ok());
assert_eq!(name.unwrap().to_string(), "my-plugin");
}
#[test]
fn valid_name_with_underscores() {
let name: Result<Name, _> = "my_plugin".parse();
assert!(name.is_ok());
assert_eq!(name.unwrap().to_string(), "my_plugin");
}
#[test]
fn valid_name_with_numbers() {
let name: Result<Name, _> = "plugin2".parse();
assert!(name.is_ok());
assert_eq!(name.unwrap().to_string(), "plugin2");
}
#[test]
fn valid_name_mixed() {
let name: Result<Name, _> = "My-Plugin_v2".parse();
assert!(name.is_ok());
assert_eq!(name.unwrap().to_string(), "My-Plugin_v2");
}
#[test]
fn valid_single_char() {
let name: Result<Name, _> = "a".parse();
assert!(name.is_ok());
}
#[test]
fn invalid_empty_name() {
let name: Result<Name, _> = "".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::Empty);
} else {
panic!("Expected InvalidName error with Empty kind");
}
}
#[test]
fn invalid_starts_with_number() {
let name: Result<Name, _> = "2plugin".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::InvalidInitialChar);
} else {
panic!("Expected InvalidName error with InvalidInitialChar kind");
}
}
#[test]
fn invalid_starts_with_hyphen() {
let name: Result<Name, _> = "-plugin".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::InvalidInitialChar);
} else {
panic!("Expected InvalidName error with InvalidInitialChar kind");
}
}
#[test]
fn invalid_starts_with_underscore() {
let name: Result<Name, _> = "_plugin".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::InvalidInitialChar);
} else {
panic!("Expected InvalidName error with InvalidInitialChar kind");
}
}
#[test]
fn invalid_contains_space() {
let name: Result<Name, _> = "my plugin".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::InvalidChar);
} else {
panic!("Expected InvalidName error with InvalidChar kind");
}
}
#[test]
fn invalid_contains_special_char() {
let name: Result<Name, _> = "my@plugin".parse();
assert!(name.is_err());
if let Err(Error::InvalidName { kind }) = name {
assert_eq!(kind, NameErrorKind::InvalidChar);
} else {
panic!("Expected InvalidName error with InvalidChar kind");
}
}
#[test]
fn as_ref_returns_str() {
let name: Name = "myplugin".parse().unwrap();
let s: &str = name.as_ref();
assert_eq!(s, "myplugin");
}
#[test]
fn into_string() {
let name: Name = "myplugin".parse().unwrap();
let s: String = name.into();
assert_eq!(s, "myplugin");
}
}