stack_string/
stack_string.rs

1use compact_str::CompactString as CompactStr;
2pub use compact_str::ToCompactString;
3use derive_more::{Deref, DerefMut, Display, From, Index, IndexMut, Into};
4use serde::{Deserialize, Serialize};
5use std::{
6    borrow::{Borrow, BorrowMut, Cow},
7    convert::Infallible,
8    ffi::OsStr,
9    fmt::{self, Write as FmtWrite},
10    iter::FromIterator,
11    path::Path,
12    str,
13    str::{FromStr, Utf8Error},
14    string::FromUtf8Error,
15};
16
17#[cfg(feature = "postgres_types")]
18use bytes::BytesMut;
19#[cfg(feature = "postgres_types")]
20use postgres_types::{FromSql, IsNull, ToSql, Type};
21
22#[cfg(feature = "utoipa_types")]
23use utoipa::{PartialSchema, ToSchema};
24
25#[cfg(feature = "axum_types")]
26use axum::response::IntoResponse;
27
28#[cfg(feature = "axum_types")]
29use axum::body::Body;
30
31#[cfg(feature = "async_graphql")]
32use async_graphql::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
33
34#[derive(
35    Display,
36    Serialize,
37    Deserialize,
38    Deref,
39    DerefMut,
40    Index,
41    IndexMut,
42    Debug,
43    Clone,
44    Into,
45    From,
46    PartialEq,
47    Eq,
48    Hash,
49    Default,
50    PartialOrd,
51    Ord,
52)]
53pub struct StackString(CompactStr);
54
55impl StackString {
56    #[must_use]
57    pub fn new() -> Self {
58        Self(CompactStr::new(""))
59    }
60
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        self.0.as_str()
64    }
65
66    #[must_use]
67    pub fn split_off(&mut self, index: usize) -> Self {
68        Self(self.0.split_off(index))
69    }
70
71    /// Construct a `StackString` from a `&[u8]`
72    /// # Errors
73    ///
74    /// Will return an Error if the byte slice is not utf8 compliant
75    pub fn from_utf8(v: &[u8]) -> Result<Self, Utf8Error> {
76        CompactStr::from_utf8(v).map(Self)
77    }
78
79    /// Construct a `StackString` from a `Vec<u8>`
80    /// # Errors
81    ///
82    /// Will return an Error if the byte slice is not utf8 compliant
83    pub fn from_utf8_vec(vec: Vec<u8>) -> Result<Self, FromUtf8Error> {
84        String::from_utf8(vec).map(|s| Self(CompactStr::from_string_buffer(s)))
85    }
86
87    #[must_use]
88    pub fn from_utf8_lossy(v: &[u8]) -> Self {
89        Self(CompactStr::from_utf8_lossy(v))
90    }
91
92    /// # Panics
93    /// `from_display` panics if a formatting trait implementation returns an
94    /// error. This indicates an incorrect implementation
95    /// since `fmt::Write for String` never returns an error itself.
96    pub fn from_display(buf: impl fmt::Display) -> Self {
97        let mut s = Self::new();
98        write!(s, "{buf}").unwrap();
99        s
100    }
101
102    #[inline]
103    #[must_use]
104    pub fn is_inline(&self) -> bool {
105        !self.is_heap_allocated()
106    }
107}
108
109impl From<StackString> for String {
110    fn from(item: StackString) -> Self {
111        item.0.into()
112    }
113}
114
115impl From<&StackString> for String {
116    fn from(item: &StackString) -> Self {
117        item.as_str().into()
118    }
119}
120
121impl From<&StackString> for StackString {
122    fn from(item: &StackString) -> Self {
123        item.clone()
124    }
125}
126
127impl From<String> for StackString {
128    fn from(item: String) -> Self {
129        Self(item.into())
130    }
131}
132
133impl From<&String> for StackString {
134    fn from(item: &String) -> Self {
135        Self(item.into())
136    }
137}
138
139impl From<&str> for StackString {
140    fn from(item: &str) -> Self {
141        Self(item.into())
142    }
143}
144
145impl<'a> From<&'a StackString> for &'a str {
146    fn from(item: &StackString) -> &str {
147        item.as_str()
148    }
149}
150
151impl<'a> From<Cow<'a, str>> for StackString {
152    fn from(item: Cow<'a, str>) -> Self {
153        match item {
154            Cow::Borrowed(s) => s.into(),
155            Cow::Owned(s) => s.into(),
156        }
157    }
158}
159
160impl From<StackString> for Cow<'_, str> {
161    fn from(item: StackString) -> Self {
162        Cow::Owned(item.into())
163    }
164}
165
166impl Borrow<str> for StackString {
167    fn borrow(&self) -> &str {
168        self.0.borrow()
169    }
170}
171
172impl BorrowMut<str> for StackString {
173    fn borrow_mut(&mut self) -> &mut str {
174        self.0.borrow_mut()
175    }
176}
177
178impl AsRef<str> for StackString {
179    fn as_ref(&self) -> &str {
180        self.0.as_str()
181    }
182}
183
184impl AsRef<[u8]> for StackString {
185    fn as_ref(&self) -> &[u8] {
186        self.0.as_ref()
187    }
188}
189
190impl AsRef<OsStr> for StackString {
191    fn as_ref(&self) -> &OsStr {
192        self.as_str().as_ref()
193    }
194}
195
196impl AsRef<Path> for StackString {
197    fn as_ref(&self) -> &Path {
198        Path::new(self)
199    }
200}
201
202impl FromStr for StackString {
203    type Err = Infallible;
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        Ok(s.into())
206    }
207}
208
209impl<'a> PartialEq<Cow<'a, str>> for StackString {
210    #[inline]
211    fn eq(&self, other: &Cow<'a, str>) -> bool {
212        PartialEq::eq(&self[..], &other[..])
213    }
214}
215
216impl<'a> PartialOrd<Cow<'a, str>> for StackString {
217    fn partial_cmp(&self, other: &Cow<'a, str>) -> Option<std::cmp::Ordering> {
218        PartialOrd::partial_cmp(&self[..], &other[..])
219    }
220}
221
222impl PartialEq<String> for StackString {
223    #[inline]
224    fn eq(&self, other: &String) -> bool {
225        PartialEq::eq(&self[..], &other[..])
226    }
227}
228
229impl PartialOrd<String> for StackString {
230    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
231        PartialOrd::partial_cmp(&self[..], &other[..])
232    }
233}
234
235impl PartialEq<str> for StackString {
236    #[inline]
237    fn eq(&self, other: &str) -> bool {
238        PartialEq::eq(&self.0, other)
239    }
240}
241
242impl PartialOrd<str> for StackString {
243    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
244        PartialOrd::partial_cmp(&self[..], other)
245    }
246}
247
248impl<'a> PartialEq<&'a str> for StackString {
249    #[inline]
250    fn eq(&self, other: &&'a str) -> bool {
251        PartialEq::eq(&self[..], &other[..])
252    }
253}
254
255impl<'a> PartialOrd<&'a str> for StackString {
256    fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
257        PartialOrd::partial_cmp(&self[..], &other[..])
258    }
259}
260
261impl PartialEq<StackString> for str {
262    fn eq(&self, other: &StackString) -> bool {
263        PartialEq::eq(self, &other[..])
264    }
265}
266
267impl PartialOrd<StackString> for str {
268    fn partial_cmp(&self, other: &StackString) -> Option<std::cmp::Ordering> {
269        PartialOrd::partial_cmp(self, &other[..])
270    }
271}
272
273impl PartialEq<StackString> for &str {
274    fn eq(&self, other: &StackString) -> bool {
275        PartialEq::eq(&self[..], &other[..])
276    }
277}
278
279impl PartialOrd<StackString> for &str {
280    fn partial_cmp(&self, other: &StackString) -> Option<std::cmp::Ordering> {
281        PartialOrd::partial_cmp(&self[..], &other[..])
282    }
283}
284
285impl FromIterator<char> for StackString {
286    fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> Self {
287        let s = CompactStr::from_iter(iter);
288        Self(s)
289    }
290}
291
292#[cfg(feature = "postgres_types")]
293impl<'a> FromSql<'a> for StackString {
294    fn from_sql(
295        ty: &Type,
296        raw: &'a [u8],
297    ) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
298        let s = <&'a str as FromSql>::from_sql(ty, raw)?;
299        Ok(s.into())
300    }
301
302    fn accepts(ty: &Type) -> bool {
303        <&'a str as FromSql>::accepts(ty)
304    }
305}
306
307#[cfg(feature = "postgres_types")]
308impl ToSql for StackString {
309    fn to_sql(
310        &self,
311        ty: &Type,
312        out: &mut BytesMut,
313    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>>
314    where
315        Self: Sized,
316    {
317        ToSql::to_sql(&self.as_str(), ty, out)
318    }
319
320    fn accepts(ty: &Type) -> bool
321    where
322        Self: Sized,
323    {
324        <String as ToSql>::accepts(ty)
325    }
326
327    fn to_sql_checked(
328        &self,
329        ty: &Type,
330        out: &mut BytesMut,
331    ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
332        self.as_str().to_sql_checked(ty, out)
333    }
334}
335
336#[cfg(feature = "utoipa_types")]
337impl PartialSchema for StackString {
338    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
339        str::schema()
340    }
341}
342
343#[cfg(feature = "utoipa_types")]
344impl ToSchema for StackString {
345    fn name() -> Cow<'static, str> {
346        str::name()
347    }
348}
349
350#[cfg(feature = "axum_types")]
351impl IntoResponse for StackString {
352    fn into_response(self) -> axum::response::Response {
353        let s: String = self.into();
354        s.into_response()
355    }
356}
357
358#[cfg(feature = "axum_types")]
359impl From<StackString> for Body {
360    fn from(value: StackString) -> Self {
361        let s: String = value.into();
362        s.into()
363    }
364}
365
366#[macro_export]
367macro_rules! format_sstr {
368    ($($arg:tt)*) => {
369        $crate::StackString::from($crate::stack_string::ToCompactString::to_compact_string(&core::format_args!($($arg)*)))
370    }
371}
372
373/// Allow StackString to be used as graphql scalar value
374#[cfg(feature = "async_graphql")]
375#[Scalar]
376impl ScalarType for StackString {
377    fn parse(value: Value) -> InputValueResult<Self> {
378        if let Value::String(s) = value {
379            let s: StackString = s.into();
380            Ok(s)
381        } else {
382            Err(InputValueError::expected_type(value))
383        }
384    }
385
386    fn is_valid(value: &Value) -> bool {
387        matches!(value, Value::String(_))
388    }
389
390    fn to_value(&self) -> Value {
391        Value::String(self.to_string())
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use rand::{rng as thread_rng, Rng};
398
399    #[cfg(feature = "async_graphql")]
400    use std::future::Future;
401
402    use crate::{StackString, MAX_INLINE};
403
404    #[test]
405    fn test_default() {
406        assert_eq!(StackString::new(), StackString::default());
407        assert_eq!(MAX_INLINE, 24);
408    }
409
410    #[test]
411    fn test_split_off() {
412        let mut s0 = "hello there".to_string();
413        let s1 = s0.split_off(3);
414        let mut s2: StackString = "hello there".into();
415        let s3 = s2.split_off(3);
416        assert_eq!(s0.as_str(), s2.as_str());
417        assert_eq!(s1.as_str(), s3.as_str());
418    }
419
420    #[test]
421    fn test_from_utf8() {
422        let mut rng = thread_rng();
423        let v: Vec<_> = (0..20).map(|_| rng.random::<u8>() & 0x7f).collect();
424        let s0 = String::from_utf8(v.clone()).unwrap();
425        let s1 = StackString::from_utf8(&v).unwrap();
426        assert_eq!(s0.as_str(), s1.as_str());
427
428        let v: Vec<_> = (0..20).map(|_| rng.random::<u8>()).collect();
429        let s0 = String::from_utf8(v.clone());
430        let s1 = StackString::from_utf8(&v);
431
432        match s0 {
433            Ok(s) => assert_eq!(s.as_str(), s1.unwrap().as_str()),
434            Err(e) => assert_eq!(e.utf8_error(), s1.unwrap_err()),
435        }
436    }
437
438    #[test]
439    fn test_from_utf8_vec() {
440        let mut rng = thread_rng();
441        let v: Vec<_> = (0..20).map(|_| rng.random::<u8>() & 0x7f).collect();
442        let s0 = String::from_utf8(v.clone()).unwrap();
443        let s1 = StackString::from_utf8_vec(v).unwrap();
444        assert_eq!(s0.as_str(), s1.as_str());
445
446        let v: Vec<_> = (0..20).map(|_| rng.random::<u8>()).collect();
447        let s0 = String::from_utf8(v.clone());
448        let s1 = StackString::from_utf8_vec(v);
449
450        match s0 {
451            Ok(s) => assert_eq!(s.as_str(), s1.unwrap().as_str()),
452            Err(e) => assert_eq!(e, s1.unwrap_err()),
453        }
454    }
455
456    #[test]
457    fn test_string_from_compact_string() {
458        let s0 = StackString::from("Hello there");
459        let s1: String = s0.clone().into();
460        assert_eq!(s0.as_str(), s1.as_str());
461    }
462
463    #[test]
464    fn test_compact_string_from_string() {
465        let s0 = String::from("Hello there");
466        let s1: StackString = s0.clone().into();
467        assert_eq!(s0.as_str(), s1.as_str());
468        let s1: StackString = (&s0).into();
469        assert_eq!(s0.as_str(), s1.as_str());
470    }
471
472    #[test]
473    fn test_borrow() {
474        use std::borrow::Borrow;
475        let s = StackString::from("Hello");
476        let st: &str = s.borrow();
477        assert_eq!(st, "Hello");
478    }
479
480    #[test]
481    fn test_as_ref() {
482        use std::path::Path;
483
484        let s = StackString::from("Hello");
485        let st: &str = s.as_ref();
486        assert_eq!(st, s.as_str());
487        let bt: &[u8] = s.as_ref();
488        assert_eq!(bt, s.as_bytes());
489        let pt: &Path = s.as_ref();
490        assert_eq!(pt, Path::new("Hello"));
491    }
492
493    #[test]
494    fn test_from_str() {
495        let s = StackString::from("Hello");
496        let st: StackString = "Hello".parse().unwrap();
497        assert_eq!(s, st);
498    }
499
500    #[test]
501    fn test_partialeq_cow() {
502        use std::path::Path;
503        let p = Path::new("Hello");
504        let ps = p.to_string_lossy();
505        let s = StackString::from("Hello");
506        assert_eq!(s, ps);
507    }
508
509    #[test]
510    fn test_partial_eq_string() {
511        assert_eq!(StackString::from("Hello"), String::from("Hello"));
512        assert_eq!(StackString::from("Hello"), "Hello");
513        assert_eq!(&StackString::from("Hello"), "Hello");
514        assert!(StackString::from("alpha") < "beta");
515        assert!("beta" > StackString::from("alpha"));
516    }
517
518    #[test]
519    fn test_from_iterator_char() {
520        let mut rng = thread_rng();
521        let v: Vec<char> = (0..20).map(|_| rng.random::<char>()).collect();
522        let s0: StackString = v.iter().map(|x| *x).collect();
523        let s1: String = v.iter().map(|x| *x).collect();
524        assert_eq!(s0, s1);
525    }
526
527    #[test]
528    fn test_contains_compact_string() {
529        let a: StackString = "hey there".into();
530        let b: StackString = "hey".into();
531        assert!(a.contains(b.as_str()));
532    }
533
534    #[test]
535    fn test_contains_char() {
536        let a: StackString = "hey there".into();
537        assert!(a.contains(' '));
538    }
539
540    #[test]
541    fn test_equality() {
542        let s: StackString = "hey".into();
543        assert_eq!(Some(&s).map(Into::into), Some("hey"));
544    }
545
546    #[cfg(feature = "postgres_types")]
547    use bytes::BytesMut;
548    #[cfg(feature = "postgres_types")]
549    use postgres_types::{FromSql, IsNull, ToSql, Type};
550
551    #[cfg(feature = "postgres_types")]
552    #[test]
553    fn test_from_sql() {
554        let raw = b"Hello There";
555        let t = Type::TEXT;
556        let s = StackString::from_sql(&t, raw).unwrap();
557        assert_eq!(s, StackString::from("Hello There"));
558
559        assert!(<StackString as FromSql>::accepts(&t));
560    }
561
562    #[cfg(feature = "postgres_types")]
563    #[test]
564    fn test_to_sql() {
565        let s = StackString::from("Hello There");
566        let t = Type::TEXT;
567        assert!(<StackString as ToSql>::accepts(&t));
568        let mut buf = BytesMut::new();
569        match s.to_sql(&t, &mut buf).unwrap() {
570            IsNull::Yes => assert!(false),
571            IsNull::No => {}
572        }
573        assert_eq!(buf.as_ref(), b"Hello There");
574    }
575
576    #[test]
577    fn test_from_display() {
578        use std::fmt::Display;
579
580        struct Test {}
581
582        impl Display for Test {
583            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584                f.write_str("THIS IS A TEST")
585            }
586        }
587
588        let t = Test {};
589        let s = StackString::from_display(t);
590        assert_eq!(s, StackString::from("THIS IS A TEST"));
591    }
592
593    #[test]
594    fn test_format_sstr() {
595        use crate::format_sstr;
596
597        let s = format_sstr!("This is a test {}", 22);
598        assert_eq!(s, StackString::from("This is a test 22"));
599    }
600
601    #[test]
602    fn test_from_utf8_lossy() {
603        let mut v = Vec::new();
604        v.extend_from_slice("this is a test".as_bytes());
605        v.push(0xff);
606        v.extend_from_slice("yes".as_bytes());
607        let s = StackString::from_utf8_lossy(&v);
608        assert_eq!(s.len(), 20);
609        assert_eq!(s.is_heap_allocated(), false);
610    }
611
612    #[test]
613    fn test_serde() {
614        use serde::Deserialize;
615
616        let s = StackString::from("HELLO");
617        let t = "HELLO";
618        let s = serde_json::to_vec(&s).unwrap();
619        let t = serde_json::to_vec(t).unwrap();
620        assert_eq!(s, t);
621
622        let s = r#"{"a": "b"}"#;
623
624        #[derive(Deserialize)]
625        struct A {
626            a: StackString,
627        }
628
629        #[derive(Deserialize)]
630        struct B {
631            a: String,
632        }
633
634        let a: A = serde_json::from_str(s).unwrap();
635        let b: B = serde_json::from_str(s).unwrap();
636        assert_eq!(a.a.as_str(), b.a.as_str());
637    }
638
639    #[cfg(feature = "async_graphql")]
640    #[test]
641    fn test_compact_string_async_graphql() {
642        use async_graphql::{
643            dataloader::{DataLoader, Loader},
644            Context, EmptyMutation, EmptySubscription, Object, Schema,
645        };
646        use async_trait::async_trait;
647        use std::{collections::HashMap, convert::Infallible};
648
649        struct StackStringLoader;
650
651        impl StackStringLoader {
652            fn new() -> Self {
653                Self
654            }
655        }
656
657        #[async_trait]
658        impl Loader<StackString> for StackStringLoader {
659            type Value = StackString;
660            type Error = Infallible;
661
662            fn load(
663                &self,
664                _: &[StackString],
665            ) -> impl Future<Output = Result<HashMap<StackString, Self::Value>, Self::Error>>
666            {
667                async move {
668                    let mut m = HashMap::new();
669                    m.insert("HELLO".into(), "WORLD".into());
670                    Ok(m)
671                }
672            }
673        }
674
675        struct QueryRoot;
676
677        #[Object]
678        impl QueryRoot {
679            async fn hello<'a>(
680                &self,
681                ctx: &Context<'a>,
682            ) -> Result<Option<StackString>, Infallible> {
683                let hello = ctx
684                    .data::<DataLoader<StackStringLoader>>()
685                    .unwrap()
686                    .load_one("hello".into())
687                    .await
688                    .unwrap();
689                Ok(hello)
690            }
691        }
692
693        let expected_sdl = include_str!("../tests/data/sdl_file_compactstring.txt");
694
695        let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
696            .data(DataLoader::new(
697                StackStringLoader::new(),
698                tokio::task::spawn,
699            ))
700            .finish();
701        let sdl = schema.sdl();
702
703        assert_eq!(&sdl, expected_sdl);
704    }
705}