use smol_str::SmolStr;
use std::{
borrow::{Borrow, Cow},
fmt,
str::FromStr,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Canonical(SmolStr);
impl Canonical {
pub fn try_new(input: &str) -> Result<Self, CanonicalError> {
let token = canonicalize(input);
if token.is_empty() {
return Err(CanonicalError::InvalidCanonicalToken {
value: input.to_string(),
});
}
Ok(Self(SmolStr::new(token.as_ref())))
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
#[must_use]
pub fn into_inner(self) -> String {
self.0.to_string()
}
}
impl fmt::Display for Canonical {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_ref())
}
}
impl AsRef<str> for Canonical {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl Borrow<str> for Canonical {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl FromStr for Canonical {
type Err = CanonicalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_new(s)
}
}
#[inline]
#[must_use]
pub fn canonicalize(input: &str) -> Cow<'_, str> {
if is_canonical(input) {
return Cow::Borrowed(input);
}
let mut out = String::with_capacity(input.len());
let mut prev_sep = true;
for ch in input.chars() {
let c = ch.to_ascii_uppercase();
if c.is_ascii_alphanumeric() {
out.push(c);
prev_sep = false;
} else if !prev_sep {
out.push('_');
prev_sep = true;
}
}
if out.ends_with('_') {
out.pop(); }
Cow::Owned(out)
}
#[inline]
fn is_canonical(input: &str) -> bool {
let b = input.as_bytes();
if b.is_empty() || b[0] == b'_' || b[b.len() - 1] == b'_' {
return false;
}
let mut prev = b'_';
for &c in b {
match c {
b'A'..=b'Z' | b'0'..=b'9' => prev = c,
b'_' if prev != b'_' => prev = c,
_ => return false,
}
}
true
}
pub trait StringCode {
fn code(&self) -> &str;
fn is_canonical(&self) -> bool {
true
}
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CanonicalError {
#[error("Invalid canonical token: '{value}' - canonicalized value must be non-empty")]
InvalidCanonicalToken {
value: String,
},
}