Skip to main content

orengine_utils/
small_string.rs

1//! A compact UTF-8 string backed by [`smallvec::SmallVec`].
2//!
3//! `SmallString` stores short strings inline without a heap allocation and
4//! transparently spills to the heap when the inline capacity is exceeded.
5//! It dereferences to `str`, making it convenient to use anywhere a string
6//! slice is expected.
7//!
8//! The type implements `serde::Serialize` and `serde::Deserialize` as a
9//! regular UTF-8 string.
10
11use alloc::string::String;
12use core::ops::Deref;
13#[cfg(not(feature = "no_std"))]
14use serde::de::{Error, Visitor};
15#[cfg(not(feature = "no_std"))]
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17use smallvec::SmallVec;
18
19/// A UTF-8 string with configurable inline storage.
20///
21/// While the length is less or equal to `INLINE_SIZE`, the string is stored
22/// inline on a stack. When the length exceeds this limit, the string spills
23/// to the heap.
24#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
25pub struct SmallString<const INLINE_SIZE: usize>(SmallVec<u8, INLINE_SIZE>);
26
27impl<const INLINE_SIZE: usize> SmallString<INLINE_SIZE> {
28    /// Creates an empty string.
29    #[must_use]
30    pub fn empty() -> Self {
31        Self(SmallVec::new())
32    }
33
34    /// Reads a string of the specified length from a reader.
35    ///
36    /// # Errors
37    ///
38    /// Returns an error if the reader fails to read.
39    #[cfg(not(feature = "no_std"))]
40    pub(crate) fn fill_from_reader<R: std::io::Read>(
41        mut reader: R,
42        len: usize,
43    ) -> Result<Self, std::io::Error> {
44        let mut res = Self(SmallVec::new());
45
46        res.0.resize(len, 0);
47        reader.read_exact(&mut res.0)?;
48
49        Ok(res)
50    }
51
52    /// Appends the provided UTF-8 bytes to the string.
53    ///
54    /// # Panics
55    ///
56    /// Panics if the resulting byte sequence is not valid UTF-8.
57    ///
58    /// This method is intended to be used only with valid UTF-8 data.
59    pub fn extend_from_slice(&mut self, bytes: &[u8]) {
60        self.0.extend_from_slice(bytes);
61
62        // Preserve the invariant that the contents are always valid UTF-8.
63        debug_assert!(core::str::from_utf8(&self.0).is_ok());
64    }
65}
66
67impl<const INLINE_SIZE: usize> From<&str> for SmallString<INLINE_SIZE> {
68    fn from(value: &str) -> Self {
69        Self(value.as_bytes().into())
70    }
71}
72
73impl<const INLINE_SIZE: usize> From<String> for SmallString<INLINE_SIZE> {
74    fn from(value: String) -> Self {
75        Self(value.into_bytes().into())
76    }
77}
78
79impl<const INLINE_SIZE: usize> Deref for SmallString<INLINE_SIZE> {
80    type Target = str;
81
82    fn deref(&self) -> &Self::Target {
83        core::str::from_utf8(&self.0).unwrap()
84    }
85}
86
87#[cfg(not(feature = "no_std"))]
88impl<const INLINE_SIZE: usize> Serialize for SmallString<INLINE_SIZE> {
89    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
90    where
91        S: Serializer,
92    {
93        serializer.serialize_str(self)
94    }
95}
96
97#[cfg(not(feature = "no_std"))]
98impl<'de, const INLINE_SIZE: usize> Deserialize<'de> for SmallString<INLINE_SIZE> {
99    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        struct SmallStringVisitor<const INLINE_SIZE: usize>;
104
105        impl<const INLINE_SIZE: usize> Visitor<'_> for SmallStringVisitor<INLINE_SIZE> {
106            type Value = SmallString<INLINE_SIZE>;
107
108            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
109                formatter.write_str("a UTF-8 string")
110            }
111
112            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
113            where
114                E: Error,
115            {
116                Ok(SmallString::from(value))
117            }
118        }
119
120        deserializer.deserialize_str(SmallStringVisitor::<INLINE_SIZE>)
121    }
122
123    fn deserialize_in_place<D>(deserializer: D, place: &mut Self) -> Result<(), D::Error>
124    where
125        D: Deserializer<'de>,
126    {
127        struct SmallStringVisitorRef<'a, const INLINE_SIZE: usize>(
128            &'a mut SmallString<INLINE_SIZE>,
129        );
130
131        impl<const INLINE_SIZE: usize> Visitor<'_> for SmallStringVisitorRef<'_, INLINE_SIZE> {
132            type Value = ();
133
134            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
135                formatter.write_str("a UTF-8 string")
136            }
137
138            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
139            where
140                E: Error,
141            {
142                self.0 .0.clear();
143                self.0 .0.extend_from_slice(value.as_bytes());
144
145                Ok(())
146            }
147        }
148
149        deserializer.deserialize_str(SmallStringVisitorRef(place))?;
150
151        Ok(())
152    }
153}
154
155impl<const INLINE_SIZE: usize> core::fmt::Debug for SmallString<INLINE_SIZE> {
156    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157        write!(f, "{}", *self)
158    }
159}
160
161impl<const INLINE_SIZE: usize> core::fmt::Display for SmallString<INLINE_SIZE> {
162    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
163        write!(f, "{}", &**self)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::SmallString;
170    use alloc::string::String;
171
172    #[test]
173    fn empty_string_is_empty() {
174        let value = SmallString::<16>::empty();
175
176        assert!(value.is_empty());
177    }
178
179    #[test]
180    fn extend_appends_bytes() {
181        let mut value = SmallString::<8>::from("hello");
182
183        value.extend_from_slice(b" world");
184
185        assert_eq!(&*value, "hello world");
186    }
187
188    #[test]
189    fn from_string_and_str_are_equal() {
190        let a = SmallString::<8>::from("example");
191        let b = SmallString::<8>::from(String::from("example"));
192
193        assert_eq!(a, b);
194    }
195
196    #[test]
197    #[cfg(not(feature = "no_std"))]
198    fn serde() {
199        use crate::rw_serde::RWDeserializer;
200        use crate::rw_serde::RWSerializer;
201        use serde::{Deserialize, Serialize};
202        use std::io::Cursor;
203
204        let value = SmallString::<8>::from("small string");
205        let mut ser = RWSerializer::new(Vec::new());
206
207        value.serialize(&mut ser).unwrap();
208
209        let buf = ser.into_inner();
210        let mut de = RWDeserializer::new(Cursor::new(buf));
211        let restored = SmallString::<8>::deserialize(&mut de).unwrap();
212
213        assert_eq!(value, restored);
214    }
215}