use alloc::borrow::Cow;
use alloc::string::String;
use crate::errors::SlotNameError;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SlotName {
name: Cow<'static, str>,
}
impl SlotName {
pub(crate) const MIN_NUM_COMPONENTS: usize = 2;
pub const fn from_static_str(name: &'static str) -> Self {
match Self::validate(name) {
Ok(()) => Self { name: Cow::Borrowed(name) },
Err(_) => panic!("invalid slot name"),
}
}
pub fn new(name: impl Into<String>) -> Result<Self, SlotNameError> {
let name = name.into();
Self::validate(&name)?;
Ok(Self { name: Cow::Owned(name) })
}
pub fn as_str(&self) -> &str {
&self.name
}
const fn validate(name: &str) -> Result<(), SlotNameError> {
let bytes = name.as_bytes();
let mut idx = 0;
let mut num_components = 0;
if bytes.is_empty() {
return Err(SlotNameError::TooShort);
}
if bytes[0] == b':' {
return Err(SlotNameError::UnexpectedColon);
} else if bytes[0] == b'_' {
return Err(SlotNameError::UnexpectedUnderscore);
}
while idx < bytes.len() {
let byte = bytes[idx];
let is_colon = byte == b':';
if is_colon {
if (idx + 1) < bytes.len() {
if bytes[idx + 1] != b':' {
return Err(SlotNameError::UnexpectedColon);
}
} else {
return Err(SlotNameError::UnexpectedColon);
}
if (idx + 2) < bytes.len() {
if bytes[idx + 2] == b':' {
return Err(SlotNameError::UnexpectedColon);
} else if bytes[idx + 2] == b'_' {
return Err(SlotNameError::UnexpectedUnderscore);
}
} else {
return Err(SlotNameError::UnexpectedColon);
}
idx += 2;
num_components += 1;
} else if Self::is_valid_char(byte) {
idx += 1;
} else {
return Err(SlotNameError::InvalidCharacter);
}
}
num_components += 1;
if num_components < Self::MIN_NUM_COMPONENTS {
return Err(SlotNameError::TooShort);
}
Ok(())
}
const fn is_valid_char(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
const FULL_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789";
const _NAME0: SlotName = SlotName::from_static_str("name::component");
const _NAME1: SlotName = SlotName::from_static_str("one::two::three::four::five");
const _NAME2: SlotName = SlotName::from_static_str("one::two_three::four");
#[test]
#[should_panic(expected = "invalid slot name")]
fn slot_name_panics_on_invalid_character() {
SlotName::from_static_str("miden!::component");
}
#[test]
#[should_panic(expected = "invalid slot name")]
fn slot_name_panics_on_invalid_character2() {
SlotName::from_static_str("miden_ö::component");
}
#[test]
#[should_panic(expected = "invalid slot name")]
fn slot_name_panics_when_too_short() {
SlotName::from_static_str("one");
}
#[test]
#[should_panic(expected = "invalid slot name")]
fn slot_name_panics_on_component_starting_with_underscores() {
SlotName::from_static_str("one::_two");
}
#[test]
fn slot_name_fails_on_invalid_colon_placement() {
assert_matches!(SlotName::new(":").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("0::1:").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new(":0::1").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("0::1:2").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("::").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("1::2::").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("::1::2").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new(":::").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("1::2:::").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new(":::1::2").unwrap_err(), SlotNameError::UnexpectedColon);
assert_matches!(SlotName::new("1::2:::3").unwrap_err(), SlotNameError::UnexpectedColon);
}
#[test]
fn slot_name_fails_on_invalid_underscore_placement() {
assert_matches!(
SlotName::new("_one::two").unwrap_err(),
SlotNameError::UnexpectedUnderscore
);
assert_matches!(
SlotName::new("one::_two").unwrap_err(),
SlotNameError::UnexpectedUnderscore
);
}
#[test]
fn slot_name_fails_on_empty_string() {
assert_matches!(SlotName::new("").unwrap_err(), SlotNameError::TooShort);
}
#[test]
fn slot_name_fails_on_single_component() {
assert_matches!(SlotName::new("single_component").unwrap_err(), SlotNameError::TooShort);
}
#[test]
fn slot_name_allows_ascii_alphanumeric_and_underscore() -> anyhow::Result<()> {
let name = format!("{FULL_ALPHABET}::second");
let slot_name = SlotName::new(&name)?;
assert_eq!(slot_name.as_str(), name);
Ok(())
}
#[test]
fn slot_name_fails_on_invalid_character() {
assert_matches!(
SlotName::new("na#me::second").unwrap_err(),
SlotNameError::InvalidCharacter
);
assert_matches!(
SlotName::new("first_entry::secönd").unwrap_err(),
SlotNameError::InvalidCharacter
);
assert_matches!(
SlotName::new("first::sec::th!rd").unwrap_err(),
SlotNameError::InvalidCharacter
);
}
#[test]
fn slot_name_with_min_components_is_valid() -> anyhow::Result<()> {
SlotName::new("miden::component")?;
Ok(())
}
#[test]
fn slot_name_with_many_components_is_valid() -> anyhow::Result<()> {
SlotName::new("miden::faucet0::fungible_1::b4sic::metadata")?;
Ok(())
}
}