fixstr/
lib.rs

1//! A small string type with fixed capacity stored on the stack
2//!
3//! # Examples
4//!
5//! ```
6//! use fixstr::FixStr;
7//!
8//! // Create a FixStr with capacity of 16 octets
9//! let tiny: FixStr<16> = FixStr::new("Hello").unwrap();
10//! assert_eq!(tiny.as_str(), "Hello");
11//! assert_eq!(tiny.capacity(), 16);
12//!
13//! // FixStr implements common traits
14//! let tiny2: FixStr<16> = "World".try_into().unwrap();
15//! let message: String = tiny2.into();
16//! ```
17
18/// A fixed-capacity string stored on the stack.
19///
20/// `FixStr<N>` stores up to N octets inline and guarantees valid UTF-8.
21/// Useful for small strings where heap allocation is undesirable.
22use std::fmt;
23use std::fmt::{Debug, Display};
24use std::marker::PhantomData;
25
26#[derive(Clone, Copy, Hash, Eq, Ord, PartialEq, PartialOrd)]
27pub struct FixStr<const N: usize> {
28    inline: [u8; N],
29    len: u8,
30    _marker: PhantomData<[u8; N]>,
31}
32
33impl<const N: usize> Debug for FixStr<N> {
34    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35        write!(f, "FixStr(\"{}\")", self.as_str())
36    }
37}
38
39impl<const N: usize> Default for FixStr<N> {
40    fn default() -> Self {
41        Self {
42            inline: [0; N],
43            len: 0,
44            _marker: PhantomData,
45        }
46    }
47}
48
49impl<const N: usize> FixStr<N> {
50    /// Creates a new `FixStr` if the input fits within capacity.
51    ///
52    /// Returns `None` if the string is too long (> N octets) or exceeds `u8::MAX`.
53    #[must_use]
54    pub fn new(s: &str) -> Option<Self> {
55        if s.len() > N || s.len() > u8::MAX as usize {
56            return None;
57        }
58
59        // UTF-8 validation not needed here since &str is already guaranteed
60        // to be valid UTF-8 by Rust's type system
61
62        let mut buffer = [0u8; N];
63        buffer[..s.len()].copy_from_slice(s.as_bytes());
64
65        u8::try_from(s.len()).ok().map(|len| Self {
66            inline: buffer,
67            len,
68            _marker: PhantomData,
69        })
70    }
71
72    /// Creates a new `FixStr` without capacity checking.
73    ///
74    /// # Panics
75    /// Panics if the string is too long for the fixed capacity.
76    #[must_use]
77    pub fn new_unchecked(s: &str) -> Self {
78        Self::new(s)
79            .unwrap_or_else(|| panic!("String '{s}' (len={}) exceeds capacity {N}", s.len()))
80    }
81
82    /// Returns a string slice containing the entire string.
83    ///
84    /// # Safety
85    /// Safe because we only store valid UTF-8 strings.
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        // SAFETY: We only store valid UTF-8 strings
89        unsafe { std::str::from_utf8_unchecked(&self.inline[..self.len as usize]) }
90    }
91
92    /// Returns the length of the string in Unicode characters.
93    ///
94    /// This may be different from the octet length for non-ASCII strings.
95    #[must_use]
96    pub fn char_len(&self) -> usize {
97        self.as_str().chars().count()
98    }
99
100    /// Returns the length of the string in octets.
101    #[must_use]
102    pub fn len(&self) -> usize {
103        self.len as usize
104    }
105
106    /// Returns true if the string is empty.
107    #[must_use]
108    pub fn is_empty(&self) -> bool {
109        self.len() == 0
110    }
111
112    /// Returns the total capacity in octets.
113    #[must_use]
114    pub fn capacity(&self) -> usize {
115        N
116    }
117}
118
119impl<const N: usize> TryFrom<&str> for FixStr<N> {
120    type Error = String;
121
122    fn try_from(s: &str) -> Result<Self, Self::Error> {
123        Self::new(s).ok_or(format!(
124            "String '{s}' (len={}) exceeds capacity {N}",
125            s.len()
126        ))
127    }
128}
129
130impl<const N: usize> TryFrom<String> for FixStr<N> {
131    type Error = String;
132
133    fn try_from(s: String) -> Result<Self, Self::Error> {
134        Self::try_from(s.as_str())
135    }
136}
137
138impl<const N: usize> From<FixStr<N>> for String {
139    fn from(s: FixStr<N>) -> Self {
140        String::from(s.as_str())
141    }
142}
143
144impl<const N: usize> AsRef<str> for FixStr<N> {
145    fn as_ref(&self) -> &str {
146        self.as_str()
147    }
148}
149
150impl<const N: usize> fmt::Display for FixStr<N> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(f, "{}", self.as_str())
153    }
154}