use std::fmt;
use std::hash::Hash;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StringId(u32);
impl StringId {
pub const LOCAL_TAG_BIT: u32 = 1 << 31;
pub const INVALID: StringId = StringId(u32::MAX);
#[inline]
#[must_use]
pub const fn new(index: u32) -> Self {
Self(index)
}
#[inline]
#[must_use]
pub const fn new_local(local_index: u32) -> Self {
Self(local_index | Self::LOCAL_TAG_BIT)
}
#[inline]
#[must_use]
pub const fn is_local(self) -> bool {
!self.is_invalid() && (self.0 & Self::LOCAL_TAG_BIT) != 0
}
#[inline]
#[must_use]
pub const fn local_index(self) -> Option<u32> {
if self.is_local() {
Some(self.0 & !Self::LOCAL_TAG_BIT)
} else {
None
}
}
#[inline]
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
#[inline]
#[must_use]
pub const fn as_usize(self) -> usize {
self.0 as usize
}
#[inline]
#[must_use]
pub const fn is_invalid(self) -> bool {
self.0 == u32::MAX
}
#[inline]
#[must_use]
pub const fn is_valid(self) -> bool {
self.0 != u32::MAX
}
}
impl fmt::Debug for StringId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_invalid() {
write!(f, "StringId(INVALID)")
} else if self.is_local() {
write!(f, "StringId(local:{})", self.local_index().unwrap_or(0))
} else {
write!(f, "StringId({})", self.0)
}
}
}
impl fmt::Display for StringId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_invalid() {
write!(f, "INVALID")
} else if self.is_local() {
write!(f, "local:{}", self.local_index().unwrap_or(0))
} else {
write!(f, "str:{}", self.0)
}
}
}
impl Default for StringId {
#[inline]
fn default() -> Self {
Self::INVALID
}
}
impl From<u32> for StringId {
#[inline]
fn from(index: u32) -> Self {
Self(index)
}
}
impl From<usize> for StringId {
#[inline]
fn from(index: usize) -> Self {
Self(u32::try_from(index).unwrap_or(u32::MAX))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_id_creation() {
let id = StringId::new(42);
assert_eq!(id.index(), 42);
assert_eq!(id.as_usize(), 42);
assert!(!id.is_invalid());
assert!(id.is_valid());
}
#[test]
fn test_string_id_invalid_sentinel() {
assert!(StringId::INVALID.is_invalid());
assert!(!StringId::INVALID.is_valid());
assert_eq!(StringId::INVALID.index(), u32::MAX);
}
#[test]
fn test_string_id_default() {
let default_id: StringId = StringId::default();
assert_eq!(default_id, StringId::INVALID);
}
#[test]
fn test_string_id_equality() {
let id1 = StringId::new(5);
let id2 = StringId::new(5);
let id3 = StringId::new(6);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_string_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(StringId::new(1));
set.insert(StringId::new(2));
set.insert(StringId::new(3));
assert!(set.contains(&StringId::new(1)));
assert!(!set.contains(&StringId::new(4)));
assert_eq!(set.len(), 3);
}
#[test]
fn test_string_id_from() {
let from_u32: StringId = 42u32.into();
assert_eq!(from_u32.index(), 42);
let from_usize: StringId = 42usize.into();
assert_eq!(from_usize.index(), 42);
}
#[test]
fn test_debug_display_format() {
let id = StringId::new(42);
assert_eq!(format!("{id:?}"), "StringId(42)");
assert_eq!(format!("{id}"), "str:42");
assert_eq!(format!("{:?}", StringId::INVALID), "StringId(INVALID)");
assert_eq!(format!("{}", StringId::INVALID), "INVALID");
}
#[test]
fn test_serde_roundtrip() {
let original = StringId::new(123);
let json = serde_json::to_string(&original).unwrap();
let deserialized: StringId = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
let bytes = postcard::to_allocvec(&original).unwrap();
let deserialized: StringId = postcard::from_bytes(&bytes).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_size_of_string_id() {
assert_eq!(std::mem::size_of::<StringId>(), 4);
}
#[test]
#[allow(clippy::clone_on_copy)] fn test_copy_clone() {
let id = StringId::new(10);
let copied = id;
let cloned = id.clone();
assert_eq!(id, copied);
assert_eq!(id, cloned);
}
}