//! Small-string optimization: inline storage for strings up to 22 bytes. //! //! `CompactString` avoids heap allocation for short strings (class names, IDs, //! short attribute values). Strings longer than 22 bytes fall back to a regular //! `String`. The enum representation is safe Rust (no `unsafe`). use std::fmt; use std::hash::{Hash, Hasher}; /// Maximum number of bytes that can be stored inline. const INLINE_CAP: usize = 22; /// A string that stores short values inline (up to 22 bytes) and falls back /// to heap allocation for longer strings. #[derive(Clone)] pub enum CompactString { /// Strings up to 22 bytes stored inline (no heap allocation). Inline { len: u8, data: [u8; INLINE_CAP] }, /// Longer strings stored on the heap. Heap(String), } impl CompactString { /// Create a new `CompactString` from a `&str`. pub fn new(s: &str) -> Self { if s.len() <= INLINE_CAP { let mut data = [0u8; INLINE_CAP]; data[..s.len()].copy_from_slice(s.as_bytes()); CompactString::Inline { len: s.len() as u8, data, } } else { CompactString::Heap(s.to_string()) } } /// Borrow the string contents as a `&str`. pub fn as_str(&self) -> &str { match self { CompactString::Inline { len, data } => { let bytes = &data[..*len as usize]; // Safety: we only ever store valid UTF-8 (from &str input). // This is guaranteed by construction — new() only accepts &str. std::str::from_utf8(bytes).expect("CompactString: invalid UTF-8") } CompactString::Heap(s) => s.as_str(), } } /// Returns true if the string is stored inline. pub fn is_inline(&self) -> bool { matches!(self, CompactString::Inline { .. }) } /// Length in bytes. pub fn len(&self) -> usize { match self { CompactString::Inline { len, .. } => *len as usize, CompactString::Heap(s) => s.len(), } } /// Returns true if the string is empty. pub fn is_empty(&self) -> bool { self.len() == 0 } /// Estimated heap bytes used (0 for inline strings). pub fn heap_bytes(&self) -> usize { match self { CompactString::Inline { .. } => 0, CompactString::Heap(s) => s.capacity(), } } } impl Default for CompactString { fn default() -> Self { CompactString::Inline { len: 0, data: [0; INLINE_CAP], } } } impl From<&str> for CompactString { fn from(s: &str) -> Self { CompactString::new(s) } } impl From for CompactString { fn from(s: String) -> Self { if s.len() <= INLINE_CAP { CompactString::new(&s) } else { CompactString::Heap(s) } } } impl AsRef for CompactString { fn as_ref(&self) -> &str { self.as_str() } } impl std::ops::Deref for CompactString { type Target = str; fn deref(&self) -> &str { self.as_str() } } impl fmt::Debug for CompactString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.as_str(), f) } } impl fmt::Display for CompactString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl PartialEq for CompactString { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } impl Eq for CompactString {} impl PartialEq for CompactString { fn eq(&self, other: &str) -> bool { self.as_str() == other } } impl PartialEq<&str> for CompactString { fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq for CompactString { fn eq(&self, other: &String) -> bool { self.as_str() == other.as_str() } } impl PartialOrd for CompactString { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for CompactString { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_str().cmp(other.as_str()) } } impl Hash for CompactString { fn hash(&self, state: &mut H) { self.as_str().hash(state); } } #[cfg(test)] mod tests { use super::*; #[test] fn short_string_is_inline() { let s = CompactString::new("div"); assert!(s.is_inline()); assert_eq!(s.as_str(), "div"); assert_eq!(s.len(), 3); assert_eq!(s.heap_bytes(), 0); } #[test] fn exactly_22_bytes_is_inline() { let s = CompactString::new("abcdefghijklmnopqrstuv"); // 22 chars assert!(s.is_inline()); assert_eq!(s.as_str(), "abcdefghijklmnopqrstuv"); } #[test] fn long_string_is_heap() { let s = CompactString::new("abcdefghijklmnopqrstuvw"); // 23 chars assert!(!s.is_inline()); assert_eq!(s.as_str(), "abcdefghijklmnopqrstuvw"); assert!(s.heap_bytes() > 0); } #[test] fn empty_string() { let s = CompactString::new(""); assert!(s.is_inline()); assert!(s.is_empty()); assert_eq!(s.len(), 0); } #[test] fn equality() { let a = CompactString::new("hello"); let b = CompactString::new("hello"); let c = CompactString::new("world"); assert_eq!(a, b); assert_ne!(a, c); assert_eq!(a, "hello"); } #[test] fn from_string() { let s = CompactString::from("short".to_string()); assert!(s.is_inline()); let long = "a".repeat(30); let s = CompactString::from(long.clone()); assert!(!s.is_inline()); assert_eq!(s.as_str(), long); } #[test] fn clone_preserves_variant() { let a = CompactString::new("inline"); let b = a.clone(); assert!(b.is_inline()); assert_eq!(a, b); } #[test] fn display_and_debug() { let s = CompactString::new("test"); assert_eq!(format!("{s}"), "test"); assert_eq!(format!("{s:?}"), "\"test\""); } #[test] fn default_is_empty() { let s = CompactString::default(); assert!(s.is_empty()); assert!(s.is_inline()); } #[test] fn hash_matches_str() { use std::collections::hash_map::DefaultHasher; let text = "hello"; let cs = CompactString::new(text); let mut h1 = DefaultHasher::new(); cs.hash(&mut h1); let mut h2 = DefaultHasher::new(); text.hash(&mut h2); assert_eq!(h1.finish(), h2.finish()); } #[test] fn deref_to_str() { let s = CompactString::new("slice me"); assert!(s.starts_with("slice")); assert!(s.ends_with("me")); } }