use core::fmt;
#[derive(Clone, Copy, Eq, Hash)]
#[repr(transparent)]
pub struct FixedString<const N: usize>(pub [u8; N]);
impl<const N: usize> FixedString<N> {
pub const fn new() -> Self {
Self([0u8; N])
}
pub fn set(&mut self, s: &str) {
self.0 = [0u8; N];
let bytes = s.as_bytes();
let mut copy_len = bytes.len().min(N);
while copy_len > 0 && !s.is_char_boundary(copy_len) {
copy_len -= 1;
}
self.0[..copy_len].copy_from_slice(&bytes[..copy_len]);
}
pub fn as_str(&self) -> &str {
let end = self.0.iter().position(|&b| b == 0).unwrap_or(N);
core::str::from_utf8(&self.0[..end]).unwrap_or("")
}
pub fn len(&self) -> usize {
self.0.iter().position(|&b| b == 0).unwrap_or(N)
}
pub fn is_empty(&self) -> bool {
self.0[0] == 0
}
pub fn clear(&mut self) {
self.0 = [0u8; N];
}
pub const fn capacity(&self) -> usize {
N
}
}
impl<const N: usize> Default for FixedString<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> PartialEq for FixedString<N> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<const N: usize> fmt::Debug for FixedString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FixedString<{}>({:?})", N, self.as_str())
}
}
impl<const N: usize> fmt::Display for FixedString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<const N: usize> serde::Serialize for FixedString<N> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de, const N: usize> serde::Deserialize<'de> for FixedString<N> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor<const M: usize>;
impl<'de, const M: usize> serde::de::Visitor<'de> for Visitor<M> {
type Value = FixedString<M>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "a string of at most {} bytes", M)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
let mut fs = FixedString::<M>::new();
fs.set(v);
Ok(fs)
}
}
deserializer.deserialize_str(Visitor::<N>)
}
}
impl<const N: usize> From<&str> for FixedString<N> {
fn from(s: &str) -> Self {
let mut fs = Self::new();
fs.set(s);
fs
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_is_empty() {
let s = FixedString::<64>::new();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
assert_eq!(s.as_str(), "");
}
#[test]
fn set_and_read() {
let mut s = FixedString::<64>::new();
s.set("Hello, world!");
assert_eq!(s.as_str(), "Hello, world!");
assert_eq!(s.len(), 13);
assert!(!s.is_empty());
}
#[test]
fn truncation_at_capacity() {
let mut s = FixedString::<4>::new();
s.set("Hello");
assert_eq!(s.as_str(), "Hell");
assert_eq!(s.len(), 4);
}
#[test]
fn truncation_at_utf8_boundary() {
let mut s = FixedString::<5>::new();
s.set("ab€x");
assert_eq!(s.as_str(), "ab€");
let mut s = FixedString::<4>::new();
s.set("ab€");
assert_eq!(s.as_str(), "ab");
}
#[test]
fn clear() {
let mut s = FixedString::<64>::new();
s.set("test");
assert!(!s.is_empty());
s.clear();
assert!(s.is_empty());
assert_eq!(s.as_str(), "");
}
#[test]
fn exact_capacity_fill() {
let mut s = FixedString::<5>::new();
s.set("abcde");
assert_eq!(s.as_str(), "abcde");
assert_eq!(s.len(), 5);
}
#[test]
fn overwrite_shorter() {
let mut s = FixedString::<64>::new();
s.set("Hello, world!");
s.set("Hi");
assert_eq!(s.as_str(), "Hi");
assert_eq!(s.len(), 2);
}
#[test]
fn equality() {
let a = FixedString::<64>::from("test");
let b = FixedString::<64>::from("test");
let c = FixedString::<64>::from("other");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn display() {
let s = FixedString::<64>::from("display me");
assert_eq!(format!("{}", s), "display me");
}
#[test]
fn debug() {
let s = FixedString::<32>::from("debug me");
let dbg = format!("{:?}", s);
assert!(dbg.contains("FixedString<32>"));
assert!(dbg.contains("debug me"));
}
#[test]
fn serde_roundtrip() {
let original = FixedString::<64>::from("serde test");
let json = serde_json::to_string(&original).unwrap();
assert_eq!(json, "\"serde test\"");
let restored: FixedString<64> = serde_json::from_str(&json).unwrap();
assert_eq!(original, restored);
}
#[test]
fn serde_truncation() {
let json = "\"this string is longer than eight bytes\"";
let s: FixedString<8> = serde_json::from_str(json).unwrap();
assert_eq!(s.as_str(), "this str");
}
#[test]
fn default_is_empty() {
let s = FixedString::<64>::default();
assert!(s.is_empty());
}
#[test]
fn copy_semantics() {
let a = FixedString::<64>::from("copy me");
let b = a; assert_eq!(a.as_str(), "copy me");
assert_eq!(b.as_str(), "copy me");
}
#[test]
fn capacity() {
let s = FixedString::<128>::new();
assert_eq!(s.capacity(), 128);
}
}