use core::borrow::Borrow;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::marker::PhantomData;
use core::ops::Deref;
use core::str::FromStr;
#[cfg(not(feature = "std"))]
use alloc::borrow::{Cow, ToOwned};
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(feature = "std")]
use std::borrow::Cow;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use smartstring::alias::String as SmartString;
use crate::domain::KeyDomain;
use crate::error::KeyParseError;
use crate::utils;
pub const DEFAULT_MAX_KEY_LENGTH: usize = 64;
pub type SplitCache<'a> = core::str::Split<'a, char>;
#[derive(Debug)]
pub struct SplitIterator<'a>(SplitCache<'a>);
impl<'a> Iterator for SplitIterator<'a> {
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
#[derive(Debug)]
pub struct Key<T: KeyDomain> {
inner: SmartString,
hash: u64,
_marker: PhantomData<T>,
}
impl<T: KeyDomain> Clone for Key<T> {
#[inline]
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
hash: self.hash,
_marker: PhantomData,
}
}
}
impl<T: KeyDomain> PartialEq for Key<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T: KeyDomain> Eq for Key<T> {}
impl<T: KeyDomain> PartialOrd for Key<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T: KeyDomain> Ord for Key<T> {
#[inline]
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.inner.cmp(&other.inner)
}
}
impl<T: KeyDomain> Hash for Key<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.hash(state);
}
}
#[cfg(feature = "serde")]
impl<T: KeyDomain> Serialize for Key<T> {
#[inline]
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.inner.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, T: KeyDomain> Deserialize<'de> for Key<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
let s = <&str>::deserialize(deserializer)?;
Key::new(s).map_err(serde::de::Error::custom)
} else {
let s = String::deserialize(deserializer)?;
Key::from_string(s).map_err(serde::de::Error::custom)
}
}
}
impl<T: KeyDomain> Key<T> {
#[inline]
pub fn new(key: impl AsRef<str>) -> Result<Self, KeyParseError> {
let key_str = key.as_ref();
Self::new_optimized(key_str)
}
fn new_optimized(key: &str) -> Result<Self, KeyParseError> {
if key.is_empty() {
return Err(KeyParseError::Empty);
}
let normalized = Self::normalize(key);
Self::validate_common(&normalized)?;
T::validate_domain_rules(&normalized).map_err(Self::fix_domain_error)?;
let hash = Self::compute_hash(&normalized);
Ok(Self {
inner: SmartString::from(normalized.as_ref()),
hash,
_marker: PhantomData,
})
}
pub fn from_string(key: String) -> Result<Self, KeyParseError> {
if key.trim().is_empty() {
return Err(KeyParseError::Empty);
}
let normalized = Self::normalize_owned(key);
Self::validate_common(&normalized)?;
T::validate_domain_rules(&normalized).map_err(Self::fix_domain_error)?;
let hash = Self::compute_hash(&normalized);
Ok(Self {
inner: SmartString::from(normalized),
hash,
_marker: PhantomData,
})
}
pub fn from_parts(parts: &[&str], delimiter: &str) -> Result<Self, KeyParseError> {
if parts.is_empty() {
return Err(KeyParseError::Empty);
}
if parts.iter().any(|part| part.is_empty()) {
return Err(KeyParseError::InvalidStructure {
reason: "Parts cannot contain empty strings",
});
}
let joined = parts.join(delimiter);
if joined.is_empty() {
return Err(KeyParseError::Empty);
}
Self::from_string(joined)
}
#[must_use]
pub fn try_from_parts(parts: &[&str], delimiter: &str) -> Option<Self> {
Self::from_parts(parts, delimiter).ok()
}
#[must_use]
pub fn from_static_unchecked(key: &'static str) -> Self {
debug_assert!(
!key.is_empty(),
"from_static_unchecked: key must not be empty"
);
debug_assert!(
key.len() <= T::MAX_LENGTH,
"from_static_unchecked: key length {} exceeds domain max {}",
key.len(),
T::MAX_LENGTH
);
let hash = Self::compute_hash(key);
Self {
inner: SmartString::from(key),
hash,
_marker: PhantomData,
}
}
pub fn try_from_static(key: &'static str) -> Result<Self, KeyParseError> {
Self::new(key)
}
#[inline]
pub fn try_new(key: impl AsRef<str>) -> Option<Self> {
Self::new(key).ok()
}
}
impl<T: KeyDomain> Key<T> {
#[must_use]
pub const fn is_valid_key_const(s: &str) -> bool {
crate::validation::is_valid_key_default(s, T::MAX_LENGTH)
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.inner
}
#[inline]
#[must_use]
pub const fn domain(&self) -> &'static str {
T::DOMAIN_NAME
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
#[must_use]
pub const fn hash(&self) -> u64 {
self.hash
}
#[inline]
#[must_use]
pub fn starts_with(&self, prefix: &str) -> bool {
self.inner.starts_with(prefix)
}
#[inline]
#[must_use]
pub fn ends_with(&self, suffix: &str) -> bool {
self.inner.ends_with(suffix)
}
#[inline]
#[must_use]
pub fn contains(&self, pattern: &str) -> bool {
self.inner.contains(pattern)
}
#[inline]
pub fn chars(&self) -> core::str::Chars<'_> {
self.inner.chars()
}
#[must_use]
pub fn split(&self, delimiter: char) -> SplitIterator<'_> {
SplitIterator(utils::new_split_cache(&self.inner, delimiter))
}
#[must_use]
pub fn split_cached(&self, delimiter: char) -> SplitCache<'_> {
utils::new_split_cache(&self.inner, delimiter)
}
#[must_use]
pub fn split_str<'a>(&'a self, delimiter: &'a str) -> core::str::Split<'a, &'a str> {
self.inner.split(delimiter)
}
pub fn ensure_prefix(&self, prefix: &str) -> Result<Self, KeyParseError> {
if self.starts_with(prefix) {
return Ok(self.clone());
}
let new_len = prefix.len() + self.len();
if new_len > T::MAX_LENGTH {
return Err(KeyParseError::TooLong {
max_length: T::MAX_LENGTH,
actual_length: new_len,
});
}
let result = utils::add_prefix_optimized(&self.inner, prefix);
Self::validate_common(&result)?;
T::validate_domain_rules(&result).map_err(Self::fix_domain_error)?;
let hash = Self::compute_hash(&result);
Ok(Self {
inner: result,
hash,
_marker: PhantomData,
})
}
pub fn ensure_suffix(&self, suffix: &str) -> Result<Self, KeyParseError> {
if self.ends_with(suffix) {
return Ok(self.clone());
}
let new_len = self.len() + suffix.len();
if new_len > T::MAX_LENGTH {
return Err(KeyParseError::TooLong {
max_length: T::MAX_LENGTH,
actual_length: new_len,
});
}
let result = utils::add_suffix_optimized(&self.inner, suffix);
Self::validate_common(&result)?;
T::validate_domain_rules(&result).map_err(Self::fix_domain_error)?;
let hash = Self::compute_hash(&result);
Ok(Self {
inner: result,
hash,
_marker: PhantomData,
})
}
#[must_use]
pub fn validation_info(&self) -> KeyValidationInfo {
KeyValidationInfo {
domain_info: crate::domain::domain_info::<T>(),
length: self.len(),
}
}
}
impl<T: KeyDomain> Key<T> {
#[inline]
fn fix_domain_error(e: KeyParseError) -> KeyParseError {
match e {
KeyParseError::DomainValidation { message, .. } => KeyParseError::DomainValidation {
domain: T::DOMAIN_NAME,
message,
},
other => other,
}
}
pub(crate) fn validate_common(key: &str) -> Result<(), KeyParseError> {
if key.is_empty() {
return Err(KeyParseError::Empty);
}
if key.len() > T::MAX_LENGTH {
return Err(KeyParseError::TooLong {
max_length: T::MAX_LENGTH,
actual_length: key.len(),
});
}
if key.len() < T::min_length() {
return Err(KeyParseError::TooShort {
min_length: T::min_length(),
actual_length: key.len(),
});
}
Self::validate_fast(key)
}
fn validate_fast(key: &str) -> Result<(), KeyParseError> {
let mut chars = key.char_indices();
let mut prev_char = None;
if let Some((pos, first)) = chars.next() {
let char_allowed = crate::utils::char_validation::is_key_char_fast(first)
|| T::allowed_start_character(first);
if !char_allowed {
return Err(KeyParseError::InvalidCharacter {
character: first,
position: pos,
expected: Some("allowed by domain"),
});
}
prev_char = Some(first);
}
for (pos, c) in chars {
let char_allowed = T::allowed_characters(c);
if !char_allowed {
return Err(KeyParseError::InvalidCharacter {
character: c,
position: pos,
expected: Some("allowed by domain"),
});
}
if let Some(prev) = prev_char {
if !T::allowed_consecutive_characters(prev, c) {
return Err(KeyParseError::InvalidStructure {
reason: "consecutive characters not allowed",
});
}
}
prev_char = Some(c);
}
if let Some(last) = prev_char {
if !T::allowed_end_character(last) {
return Err(KeyParseError::InvalidStructure {
reason: "invalid end character",
});
}
}
Ok(())
}
pub(crate) fn normalize(key: &str) -> Cow<'_, str> {
let trimmed = key.trim();
let needs_lowercase =
T::CASE_INSENSITIVE && trimmed.chars().any(|c| c.is_ascii_uppercase());
let base = if needs_lowercase {
Cow::Owned(trimmed.to_ascii_lowercase())
} else {
Cow::Borrowed(trimmed)
};
T::normalize_domain(base)
}
fn normalize_owned(mut key: String) -> String {
let start = key.len() - key.trim_start().len();
if start > 0 {
key.drain(..start);
}
let trimmed_len = key.trim_end().len();
key.truncate(trimmed_len);
if T::CASE_INSENSITIVE {
key.make_ascii_lowercase();
}
match T::normalize_domain(Cow::Owned(key)) {
Cow::Owned(s) => s,
Cow::Borrowed(s) => s.to_owned(),
}
}
pub(crate) fn compute_hash(key: &str) -> u64 {
if key.is_empty() {
return 0;
}
Self::compute_hash_inner(key.as_bytes())
}
fn compute_hash_inner(bytes: &[u8]) -> u64 {
#[cfg(feature = "fast")]
{
#[cfg(any(
all(target_arch = "x86_64", target_feature = "aes"),
all(
target_arch = "aarch64",
target_feature = "aes",
target_feature = "neon"
)
))]
{
gxhash::gxhash64(bytes, 0)
}
#[cfg(not(any(
all(target_arch = "x86_64", target_feature = "aes"),
all(
target_arch = "aarch64",
target_feature = "aes",
target_feature = "neon"
)
)))]
{
use core::hash::Hasher;
let mut hasher = ahash::AHasher::default();
hasher.write(bytes);
hasher.finish()
}
}
#[cfg(all(feature = "secure", not(feature = "fast")))]
{
use core::hash::Hasher;
let mut hasher = ahash::AHasher::default();
hasher.write(bytes);
hasher.finish()
}
#[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
{
let hash = blake3::hash(bytes);
let h = hash.as_bytes();
u64::from_le_bytes([h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]])
}
#[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
{
#[cfg(feature = "std")]
{
use core::hash::Hasher;
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
hasher.write(bytes);
hasher.finish()
}
#[cfg(not(feature = "std"))]
{
Self::fnv1a_hash(bytes)
}
}
}
#[cfg_attr(
any(
feature = "std",
feature = "fast",
feature = "secure",
feature = "crypto"
),
expect(
dead_code,
reason = "only used in the default hash path without `fast`/`secure`/`crypto`; other features or `std` use a different hasher"
)
)]
fn fnv1a_hash(bytes: &[u8]) -> u64 {
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
let mut hash = FNV_OFFSET_BASIS;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyValidationInfo {
pub domain_info: crate::domain::DomainInfo,
pub length: usize,
}
impl<T: KeyDomain> fmt::Display for Key<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner)
}
}
impl<T: KeyDomain> Deref for Key<T> {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.inner
}
}
impl<T: KeyDomain> AsRef<str> for Key<T> {
#[inline]
fn as_ref(&self) -> &str {
self
}
}
impl<T: KeyDomain> Borrow<str> for Key<T> {
#[inline]
fn borrow(&self) -> &str {
self
}
}
impl<T: KeyDomain> From<Key<T>> for String {
fn from(key: Key<T>) -> Self {
key.inner.into()
}
}
impl<T: KeyDomain> From<SmartString> for Key<T> {
#[inline]
fn from(inner: SmartString) -> Self {
let hash = Self::compute_hash(&inner);
Self {
inner,
hash,
_marker: PhantomData,
}
}
}
impl<T: KeyDomain> TryFrom<String> for Key<T> {
type Error = KeyParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Key::from_string(s)
}
}
impl<T: KeyDomain> TryFrom<&str> for Key<T> {
type Error = KeyParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Key::new(s)
}
}
impl<T: KeyDomain> FromStr for Key<T> {
type Err = KeyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Key::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{DefaultDomain, Domain};
#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Debug)]
struct TestDomain;
impl Domain for TestDomain {
const DOMAIN_NAME: &'static str = "test";
}
impl KeyDomain for TestDomain {
const MAX_LENGTH: usize = 32;
const HAS_CUSTOM_VALIDATION: bool = true;
const HAS_CUSTOM_NORMALIZATION: bool = true;
const CASE_INSENSITIVE: bool = true;
fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
if key.starts_with("invalid_") {
return Err(KeyParseError::domain_error(
Self::DOMAIN_NAME,
"Keys cannot start with 'invalid_'",
));
}
Ok(())
}
fn normalize_domain(key: Cow<'_, str>) -> Cow<'_, str> {
if key.contains('-') {
Cow::Owned(key.replace('-', "_"))
} else {
key
}
}
fn allowed_characters(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn validation_help() -> Option<&'static str> {
Some("Use alphanumeric characters, underscores, and hyphens. Cannot start with 'invalid_'.")
}
}
type TestKey = Key<TestDomain>;
#[test]
fn new_key_stores_value_and_domain() {
let key = TestKey::new("valid_key").unwrap();
assert_eq!(key.as_str(), "valid_key");
assert_eq!(key.domain(), "test");
assert_eq!(key.len(), 9);
}
#[test]
fn case_insensitive_domain_lowercases_and_normalizes() {
let key = TestKey::new("Test-Key").unwrap();
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn domain_rules_reject_invalid_prefix() {
let result = TestKey::new("invalid_key");
assert!(result.is_err());
if let Err(KeyParseError::DomainValidation { domain, message }) = result {
assert_eq!(domain, "test");
assert!(message.contains("invalid_"));
} else {
panic!("Expected domain validation error");
}
}
#[test]
fn rejects_empty_too_long_and_invalid_characters() {
assert!(matches!(TestKey::new(""), Err(KeyParseError::Empty)));
let long_key = "a".repeat(50);
assert!(matches!(
TestKey::new(&long_key),
Err(KeyParseError::TooLong {
max_length: 32,
actual_length: 50
})
));
let result = TestKey::new("key with spaces");
assert!(matches!(
result,
Err(KeyParseError::InvalidCharacter {
character: ' ',
position: 3,
..
})
));
}
#[test]
fn equal_keys_produce_same_hash() {
use core::hash::{Hash, Hasher};
let key1 = TestKey::new("test_key").unwrap();
let key2 = TestKey::new("test_key").unwrap();
assert_eq!(key1.hash(), key2.hash());
let key3 = TestKey::new("different_key").unwrap();
assert_ne!(key1.hash(), key3.hash());
#[cfg(feature = "std")]
{
let mut h = std::collections::hash_map::DefaultHasher::new();
Hash::hash(&key1, &mut h);
let key_trait_hash = h.finish();
let mut h = std::collections::hash_map::DefaultHasher::new();
Hash::hash(key1.as_str(), &mut h);
let str_trait_hash = h.finish();
assert_eq!(key_trait_hash, str_trait_hash);
}
}
#[test]
fn string_query_methods_work_correctly() {
let key = TestKey::new("test_key_example").unwrap();
assert!(key.starts_with("test_"));
assert!(key.ends_with("_example"));
assert!(key.contains("_key_"));
assert_eq!(key.len(), 16);
assert!(!key.is_empty());
}
#[test]
fn from_string_validates_owned_input() {
let key = TestKey::from_string("test_key".to_string()).unwrap();
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn try_from_static_rejects_empty_string() {
let key = TestKey::try_from_static("static_key").unwrap();
assert_eq!(key.as_str(), "static_key");
let invalid = TestKey::try_from_static("");
assert!(invalid.is_err());
}
#[test]
fn validation_info_reflects_domain_config() {
let key = TestKey::new("test_key").unwrap();
let info = key.validation_info();
assert_eq!(info.domain_info.name, "test");
assert_eq!(info.domain_info.max_length, 32);
assert_eq!(info.length, 8);
assert!(info.domain_info.has_custom_validation);
assert!(info.domain_info.has_custom_normalization);
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_preserves_key() {
let key = TestKey::new("test_key").unwrap();
let json = serde_json::to_string(&key).unwrap();
assert_eq!(json, r#""test_key""#);
let deserialized: TestKey = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, key);
}
#[test]
fn from_parts_joins_and_splits_roundtrip() {
let key = TestKey::from_parts(&["user", "123", "profile"], "_").unwrap();
assert_eq!(key.as_str(), "user_123_profile");
let parts: Vec<&str> = key.split('_').collect();
assert_eq!(parts, vec!["user", "123", "profile"]);
}
#[test]
fn ensure_prefix_suffix_is_idempotent() {
let key = TestKey::new("profile").unwrap();
let prefixed = key.ensure_prefix("user_").unwrap();
assert_eq!(prefixed.as_str(), "user_profile");
let same = prefixed.ensure_prefix("user_").unwrap();
assert_eq!(same.as_str(), "user_profile");
let suffixed = key.ensure_suffix("_v1").unwrap();
assert_eq!(suffixed.as_str(), "profile_v1");
let same = suffixed.ensure_suffix("_v1").unwrap();
assert_eq!(same.as_str(), "profile_v1");
}
#[test]
fn display_shows_raw_key_value() {
let key = TestKey::new("example").unwrap();
assert_eq!(format!("{key}"), "example");
}
#[test]
fn into_string_extracts_value() {
let key = TestKey::new("example").unwrap();
let string: String = key.into();
assert_eq!(string, "example");
}
#[test]
fn parse_str_creates_validated_key() {
let key: TestKey = "example".parse().unwrap();
assert_eq!(key.as_str(), "example");
}
#[test]
fn default_domain_accepts_simple_keys() {
type DefaultKey = Key<DefaultDomain>;
let key = DefaultKey::new("test_key").unwrap();
assert_eq!(key.domain(), "default");
assert_eq!(key.as_str(), "test_key");
}
#[test]
fn len_returns_consistent_cached_value() {
let key = TestKey::new("test_key").unwrap();
assert_eq!(key.len(), 8);
assert_eq!(key.len(), 8); }
#[test]
fn split_methods_produce_same_parts() {
let key = TestKey::new("user_profile_settings").unwrap();
let parts: Vec<&str> = key.split('_').collect();
assert_eq!(parts, vec!["user", "profile", "settings"]);
let cached_parts: Vec<&str> = key.split_cached('_').collect();
assert_eq!(cached_parts, vec!["user", "profile", "settings"]);
let str_parts: Vec<&str> = key.split_str("_").collect();
assert_eq!(str_parts, vec!["user", "profile", "settings"]);
}
#[test]
fn deref_coerces_to_str() {
fn takes_str(s: &str) -> &str {
s
}
let key = TestKey::new("hello").unwrap();
let s: &str = &key;
assert_eq!(s, "hello");
assert_eq!(takes_str(&key), "hello");
}
#[test]
fn from_smartstring_creates_key_without_revalidation() {
use smartstring::alias::String as SmartString;
let smart = SmartString::from("pre_validated");
let key: TestKey = TestKey::from(smart);
assert_eq!(key.as_str(), "pre_validated");
assert_eq!(key.len(), 13);
assert_ne!(key.hash(), 0);
}
#[cfg(feature = "std")]
#[test]
fn borrow_str_enables_hashmap_get_by_str() {
use std::collections::HashMap;
let mut map: HashMap<TestKey, u32> = HashMap::new();
let key = TestKey::new("lookup_test").unwrap();
map.insert(key, 42);
assert_eq!(map.get("lookup_test"), Some(&42));
assert_eq!(map.get("nonexistent"), None);
}
#[test]
fn struct_is_32_bytes() {
assert_eq!(core::mem::size_of::<TestKey>(), 32);
}
}