use regex::Regex;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum IdError {
#[error("id cannot be emapty")]
Empty,
#[error("kind contains invalid character(s): {0}")]
InvalidCharacter(String),
#[error("name part is too long: {0}")]
NameTooLong(String),
#[error("unknown error")]
Unknown,
}
const KIND_AND_NAME_SEPARATOR: &str = "/";
lazy_static::lazy_static! {
static ref KIND_REGEX: Regex = Regex::new(r"^[a-zA-Z][0-9a-zA-Z_]{0,62}$").unwrap();
static ref NAME_REGEX: Regex = Regex::new(r"^[^\p{Z}\p{C}\p{S}]+$").unwrap();
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Kind {
name: String,
}
impl Kind {
pub fn new(ty: &str) -> Result<Self, IdError> {
if ty.is_empty() {
return Err(IdError::Empty);
}
if !KIND_REGEX.is_match(ty) {
return Err(IdError::InvalidCharacter(ty.to_string()));
}
Ok(Kind {
name: ty.to_string(),
})
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
pub struct ID {
kind_val: Kind,
name_val: String,
}
impl ID {
pub fn new(kind_val: Kind) -> Self {
ID {
kind_val,
name_val: String::new(),
}
}
pub fn new_with_name(kind_val: Kind, name_val: &str) -> Result<Self, IdError> {
validate_name(name_val)?;
Ok(ID {
kind_val,
name_val: name_val.to_string(),
})
}
pub fn new_with_str(kind_and_name: &str) -> Result<Self, IdError> {
let (kind, name) = kind_and_name
.split_once(KIND_AND_NAME_SEPARATOR)
.unwrap_or(("", ""));
if kind.is_empty() {
return Err(IdError::Empty);
}
let kind_val = Kind::new(kind)?;
ID::new_with_name(kind_val, name)
}
pub fn kind(&self) -> &Kind {
&self.kind_val
}
pub fn name(&self) -> &str {
&self.name_val
}
pub fn marshal_text(&self) -> String {
self.to_string()
}
}
impl fmt::Display for ID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.name_val.is_empty() {
write!(f, "{}", self.kind_val)
} else {
write!(
f,
"{}{}{}",
self.kind_val, KIND_AND_NAME_SEPARATOR, self.name_val
)
}
}
}
fn validate_name(name_str: &str) -> Result<(), IdError> {
if name_str.is_empty() {
return Ok(());
}
if name_str.len() > 1024 {
return Err(IdError::NameTooLong(name_str.to_string()));
}
if !NAME_REGEX.is_match(name_str) {
return Err(IdError::InvalidCharacter(name_str.to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kind_creation() {
let valid_kind = Kind::new("validKind").unwrap();
assert_eq!(valid_kind.to_string(), "validKind");
assert!(Kind::new("").is_err());
assert!(Kind::new("123Invalid").is_err());
}
#[test]
fn test_id_creation() {
let kind_val = Kind::new("validKind").unwrap();
let id = ID::new(kind_val.clone());
assert_eq!(id.kind().to_string(), "validKind");
assert!(id.name().is_empty());
let id_with_name = ID::new_with_name(kind_val.clone(), "validName").unwrap();
assert_eq!(id_with_name.name(), "validName");
assert!(ID::new_with_name(kind_val.clone(), "").is_ok());
assert!(ID::new_with_name(kind_val.clone(), "Invalid Name!").is_err());
}
}