Skip to main content

byteview/
strview.rs

1// Copyright (c) 2024-present, fjall-rs
2// This source code is licensed under both the Apache 2.0 and MIT License
3// (found in the LICENSE-* files in the repository)
4
5use crate::ByteView;
6use std::{ops::Deref, sync::Arc};
7
8/// An immutable, UTF-8–encoded string slice
9///
10/// Will be inlined (no pointer dereference or heap allocation)
11/// if it is 20 characters or shorter (on a 64-bit system).
12///
13/// A single heap allocation will be shared between multiple strings.
14/// Even substrings of that heap allocation can be cloned without additional heap allocation.
15///
16/// Uses [`ByteView`] internally, but derefs as [`&str`].
17#[repr(C)]
18#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
19pub struct StrView(ByteView);
20
21impl std::fmt::Display for StrView {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{}", &**self)
24    }
25}
26
27impl std::fmt::Debug for StrView {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{:?}", &**self)
30    }
31}
32
33impl Deref for StrView {
34    type Target = str;
35
36    fn deref(&self) -> &Self::Target {
37        // SAFETY: Constructor takes a &str
38        unsafe { std::str::from_utf8_unchecked(&self.0) }
39    }
40}
41
42impl std::hash::Hash for StrView {
43    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
44        self.deref().hash(state);
45    }
46}
47
48impl StrView {
49    /// Creates a new string from an existing byte string.
50    ///
51    /// Will heap-allocate the string if it has at least length 13.
52    ///
53    /// # Panics
54    ///
55    /// Panics if the length does not fit in a u32 (4 GiB).
56    #[must_use]
57    pub fn new(s: &str) -> Self {
58        Self(ByteView::new(s.as_bytes()))
59    }
60
61    #[doc(hidden)]
62    #[must_use]
63    #[allow(clippy::missing_const_for_fn)]
64    pub unsafe fn from_raw(view: ByteView) -> Self {
65        Self(view)
66    }
67
68    /// Clones the contents of this string into an independently tracked string.
69    #[must_use]
70    pub fn to_detached(&self) -> Self {
71        Self::new(self)
72    }
73
74    /// Clones the given range of the existing string without heap allocation.
75    #[must_use]
76    pub fn slice(&self, range: impl std::ops::RangeBounds<usize>) -> Self {
77        Self(self.0.slice(range))
78    }
79
80    /// Returns `true` if the string is empty.
81    #[must_use]
82    pub fn is_empty(&self) -> bool {
83        self.0.is_empty()
84    }
85
86    /// Returns the amount of bytes in the string.
87    #[must_use]
88    pub fn len(&self) -> usize {
89        self.0.len()
90    }
91
92    /// Returns `true` if `needle` is a prefix of the string or equal to the string.
93    #[must_use]
94    pub fn starts_with(&self, needle: &str) -> bool {
95        self.0.starts_with(needle.as_bytes())
96    }
97}
98
99impl std::borrow::Borrow<str> for StrView {
100    fn borrow(&self) -> &str {
101        self
102    }
103}
104
105impl AsRef<str> for StrView {
106    fn as_ref(&self) -> &str {
107        self
108    }
109}
110
111impl From<&str> for StrView {
112    fn from(value: &str) -> Self {
113        Self::new(value)
114    }
115}
116
117impl From<String> for StrView {
118    fn from(value: String) -> Self {
119        Self::new(&value)
120    }
121}
122
123impl From<Arc<str>> for StrView {
124    fn from(value: Arc<str>) -> Self {
125        Self::new(&value)
126    }
127}
128
129impl TryFrom<ByteView> for StrView {
130    type Error = std::str::Utf8Error;
131
132    fn try_from(value: ByteView) -> Result<Self, Self::Error> {
133        std::str::from_utf8(&value)?;
134        Ok(Self(value))
135    }
136}
137
138impl From<StrView> for ByteView {
139    fn from(val: StrView) -> Self {
140        val.0
141    }
142}
143
144#[cfg(feature = "serde")]
145mod serde {
146    use super::StrView;
147    use serde::de::{self, Visitor};
148    use serde::{Deserialize, Deserializer, Serialize, Serializer};
149    use std::fmt;
150
151    impl Serialize for StrView {
152        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153        where
154            S: Serializer,
155        {
156            serializer.serialize_str(self.as_ref())
157        }
158    }
159
160    impl<'de> Deserialize<'de> for StrView {
161        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162        where
163            D: Deserializer<'de>,
164        {
165            struct StrViewVisitor;
166
167            impl Visitor<'_> for StrViewVisitor {
168                type Value = StrView;
169
170                fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
171                    formatter.write_str("a string")
172                }
173
174                fn visit_str<E>(self, v: &str) -> Result<StrView, E>
175                where
176                    E: de::Error,
177                {
178                    Ok(StrView::new(v))
179                }
180            }
181
182            deserializer.deserialize_str(StrViewVisitor)
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::StrView;
190    use std::collections::HashMap;
191
192    #[cfg(feature = "serde")]
193    #[test]
194    fn serde_roundtrip() -> serde_json::Result<()> {
195        let a = StrView::from("abcdef");
196        let b: StrView = serde_json::from_slice(&serde_json::to_vec(&a)?)?;
197        assert_eq!(a, b);
198        Ok(())
199    }
200
201    #[test]
202    fn strview_hash() {
203        let a = StrView::from("abcdef");
204
205        let mut map = HashMap::new();
206        map.insert(a, 0);
207        assert!(map.contains_key("abcdef"));
208    }
209
210    #[test]
211    fn cmp_misc_1() {
212        let a = StrView::from("abcdef");
213        let b = StrView::from("abcdefhelloworldhelloworld");
214        assert!(a < b);
215    }
216
217    #[test]
218    fn nostr() {
219        let slice = StrView::from("");
220        assert_eq!(0, slice.len());
221        assert_eq!(&*slice, "");
222    }
223
224    #[test]
225    fn default_str() {
226        let slice = StrView::default();
227        assert_eq!(0, slice.len());
228        assert_eq!(&*slice, "");
229    }
230
231    #[test]
232    fn short_str() {
233        let slice = StrView::from("abcdef");
234        assert_eq!(6, slice.len());
235        assert_eq!(&*slice, "abcdef");
236    }
237
238    #[test]
239    #[cfg(target_pointer_width = "64")]
240    fn medium_str() {
241        let slice = StrView::from("abcdefabcdef");
242        assert_eq!(12, slice.len());
243        assert_eq!(&*slice, "abcdefabcdef");
244    }
245
246    #[test]
247    #[cfg(target_pointer_width = "64")]
248    fn medium_long_str() {
249        let slice = StrView::from("abcdefabcdefabcdabcd");
250        assert_eq!(20, slice.len());
251        assert_eq!(&*slice, "abcdefabcdefabcdabcd");
252    }
253
254    #[test]
255    #[cfg(target_pointer_width = "64")]
256    fn medium_str_clone() {
257        let slice = StrView::from("abcdefabcdefabcdefa");
258
259        #[allow(clippy::redundant_clone)]
260        let copy = slice.clone();
261
262        assert_eq!(slice, copy);
263    }
264
265    #[test]
266    fn long_str() {
267        let slice = StrView::from("abcdefabcdefabcdefababcd");
268        assert_eq!(24, slice.len());
269        assert_eq!(&*slice, "abcdefabcdefabcdefababcd");
270    }
271
272    #[test]
273    fn long_str_clone() {
274        let slice = StrView::from("abcdefabcdefabcdefababcd");
275
276        #[allow(clippy::redundant_clone)]
277        let copy = slice.clone();
278
279        assert_eq!(slice, copy);
280    }
281
282    #[test]
283    fn long_str_slice_full() {
284        let slice = StrView::from("helloworld_thisisalongstring");
285
286        let copy = slice.slice(..);
287        assert_eq!(copy, slice);
288    }
289
290    #[test]
291    #[cfg(target_pointer_width = "64")]
292    fn long_str_slice() {
293        let slice = StrView::from("helloworld_thisisalongstring");
294
295        let copy = slice.slice(11..);
296        assert_eq!("thisisalongstring", &*copy);
297    }
298
299    #[test]
300    #[cfg(target_pointer_width = "64")]
301    fn long_str_slice_twice() {
302        let slice = StrView::from("helloworld_thisisalongstring");
303
304        let copy = slice.slice(11..);
305        assert_eq!("thisisalongstring", &*copy);
306
307        let copycopy = copy.slice(..);
308        assert_eq!(copy, copycopy);
309    }
310
311    #[test]
312    #[cfg(target_pointer_width = "64")]
313    fn long_str_slice_downgrade() {
314        let slice = StrView::from("helloworld_thisisalongstring");
315
316        let copy = slice.slice(11..);
317        assert_eq!("thisisalongstring", &*copy);
318
319        let copycopy = copy.slice(0..4);
320        assert_eq!("this", &*copycopy);
321
322        {
323            let copycopy = copy.slice(0..=4);
324            assert_eq!("thisi", &*copycopy);
325            assert_eq!(Some('t'), copycopy.chars().next());
326        }
327    }
328
329    #[test]
330    fn short_str_clone() {
331        let slice = StrView::from("abcdef");
332        let copy = slice.clone();
333        assert_eq!(slice, copy);
334
335        drop(slice);
336        assert_eq!(&*copy, "abcdef");
337    }
338
339    #[test]
340    fn short_str_slice_full() {
341        let slice = StrView::from("abcdef");
342        let copy = slice.slice(..);
343        assert_eq!(slice, copy);
344
345        drop(slice);
346        assert_eq!(&*copy, "abcdef");
347    }
348
349    #[test]
350    fn short_str_slice_part() {
351        let slice = StrView::from("abcdef");
352        let copy = slice.slice(3..);
353
354        drop(slice);
355        assert_eq!(&*copy, "def");
356    }
357
358    #[test]
359    fn short_str_slice_empty() {
360        let slice = StrView::from("abcdef");
361        let copy = slice.slice(0..0);
362
363        drop(slice);
364        assert_eq!(&*copy, "");
365    }
366
367    #[test]
368    fn tiny_str_starts_with() {
369        let a = StrView::from("abc");
370        assert!(a.starts_with("ab"));
371        assert!(!a.starts_with("b"));
372    }
373
374    #[test]
375    fn long_str_starts_with() {
376        let a = StrView::from("abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef");
377        assert!(a.starts_with("abcdef"));
378        assert!(!a.starts_with("def"));
379    }
380
381    #[test]
382    fn tiny_str_cmp() {
383        let a = StrView::from("abc");
384        let b = StrView::from("def");
385        assert!(a < b);
386    }
387
388    #[test]
389    fn tiny_str_eq() {
390        let a = StrView::from("abc");
391        let b = StrView::from("def");
392        assert_ne!(a, b);
393    }
394
395    #[test]
396    fn long_str_eq() {
397        let a = StrView::from("abcdefabcdefabcdefabcdef");
398        let b = StrView::from("xycdefabcdefabcdefabcdef");
399        assert_ne!(a, b);
400    }
401
402    #[test]
403    fn long_str_cmp() {
404        let a = StrView::from("abcdefabcdefabcdefabcdef");
405        let b = StrView::from("xycdefabcdefabcdefabcdef");
406        assert!(a < b);
407    }
408
409    #[test]
410    fn long_str_eq_2() {
411        let a = StrView::from("abcdefabcdefabcdefabcdef");
412        let b = StrView::from("abcdefabcdefabcdefabcdef");
413        assert_eq!(a, b);
414    }
415
416    #[test]
417    fn long_str_cmp_2() {
418        let a = StrView::from("abcdefabcdefabcdefabcdef");
419        let b = StrView::from("abcdefabcdefabcdefabcdeg");
420        assert!(a < b);
421    }
422
423    #[test]
424    fn long_str_cmp_3() {
425        let a = StrView::from("abcdefabcdefabcdefabcde");
426        let b = StrView::from("abcdefabcdefabcdefabcdef");
427        assert!(a < b);
428    }
429}