jacquard_common/
cowstr.rs

1use serde::{Deserialize, Serialize};
2use smol_str::SmolStr;
3use std::{
4    borrow::Cow,
5    fmt,
6    hash::{Hash, Hasher},
7    ops::Deref,
8};
9
10use crate::IntoStatic;
11
12/// A copy-on-write immutable string type that uses [`SmolStr`] for
13/// the "owned" variant.
14///
15/// The standard [`Cow`] type cannot be used, since
16/// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`.
17///
18/// Shamelessly ported from [merde](https://github.com/bearcove/merde)
19#[derive(Clone)]
20pub enum CowStr<'s> {
21    /// &str varaiant
22    Borrowed(&'s str),
23    /// Smolstr variant
24    Owned(SmolStr),
25}
26
27impl CowStr<'static> {
28    /// Create a new `CowStr` by copying from a `&str` — this might allocate
29    /// if the string is longer than `MAX_INLINE_SIZE`.
30    pub fn copy_from_str(s: &str) -> Self {
31        Self::Owned(SmolStr::from(s))
32    }
33
34    /// Create a new owned `CowStr` from a static &str without allocating
35    pub fn new_static(s: &'static str) -> Self {
36        Self::Owned(SmolStr::new_static(s))
37    }
38}
39
40impl<'s> CowStr<'s> {
41    #[inline]
42    /// Borrow and decode a byte slice as utf8 into a CowStr
43    pub fn from_utf8(s: &'s [u8]) -> Result<Self, std::str::Utf8Error> {
44        Ok(Self::Borrowed(std::str::from_utf8(s)?))
45    }
46
47    #[inline]
48    /// Take bytes and decode them as utf8 into an owned CowStr. Might allocate.
49    pub fn from_utf8_owned(s: impl AsRef<[u8]>) -> Result<Self, std::str::Utf8Error> {
50        Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s.as_ref())?)))
51    }
52
53    #[inline]
54    /// Take bytes and decode them as utf8, skipping invalid characters, taking ownership.
55    /// Will allocate, uses String::from_utf8_lossy() internally for now.
56    pub fn from_utf8_lossy(s: &'s [u8]) -> Self {
57        Self::Owned(String::from_utf8_lossy(&s).into())
58    }
59
60    /// # Safety
61    ///
62    /// This function is unsafe because it does not check that the bytes are valid UTF-8.
63    #[inline]
64    pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self {
65        unsafe { Self::Owned(SmolStr::new(std::str::from_utf8_unchecked(s))) }
66    }
67
68    /// Returns a reference to the underlying string slice.
69    #[inline]
70    pub fn as_str(&self) -> &str {
71        match self {
72            CowStr::Borrowed(s) => s,
73            CowStr::Owned(s) => s.as_str(),
74        }
75    }
76}
77
78impl AsRef<str> for CowStr<'_> {
79    #[inline]
80    fn as_ref(&self) -> &str {
81        match self {
82            CowStr::Borrowed(s) => s,
83            CowStr::Owned(s) => s.as_str(),
84        }
85    }
86}
87
88impl Deref for CowStr<'_> {
89    type Target = str;
90
91    #[inline]
92    fn deref(&self) -> &Self::Target {
93        match self {
94            CowStr::Borrowed(s) => s,
95            CowStr::Owned(s) => s.as_str(),
96        }
97    }
98}
99
100impl<'a> From<Cow<'a, str>> for CowStr<'a> {
101    #[inline]
102    fn from(s: Cow<'a, str>) -> Self {
103        match s {
104            Cow::Borrowed(s) => CowStr::Borrowed(s),
105            #[allow(clippy::useless_conversion)]
106            Cow::Owned(s) => CowStr::Owned(s.into()),
107        }
108    }
109}
110
111impl<'s> From<&'s str> for CowStr<'s> {
112    #[inline]
113    fn from(s: &'s str) -> Self {
114        CowStr::Borrowed(s)
115    }
116}
117
118impl Default for CowStr<'_> {
119    #[inline]
120    fn default() -> Self {
121        CowStr::new_static("")
122    }
123}
124
125impl From<String> for CowStr<'_> {
126    #[inline]
127    fn from(s: String) -> Self {
128        #[allow(clippy::useless_conversion)]
129        CowStr::Owned(s.into())
130    }
131}
132
133impl From<Box<str>> for CowStr<'_> {
134    #[inline]
135    fn from(s: Box<str>) -> Self {
136        CowStr::Owned(s.into())
137    }
138}
139
140impl<'s> From<&'s String> for CowStr<'s> {
141    #[inline]
142    fn from(s: &'s String) -> Self {
143        CowStr::Borrowed(s.as_str())
144    }
145}
146
147impl From<CowStr<'_>> for String {
148    #[inline]
149    fn from(s: CowStr<'_>) -> Self {
150        match s {
151            CowStr::Borrowed(s) => s.into(),
152            #[allow(clippy::useless_conversion)]
153            CowStr::Owned(s) => s.into(),
154        }
155    }
156}
157
158impl From<CowStr<'_>> for SmolStr {
159    #[inline]
160    fn from(s: CowStr<'_>) -> Self {
161        match s {
162            CowStr::Borrowed(s) => SmolStr::new(s),
163            CowStr::Owned(s) => SmolStr::new(s),
164        }
165    }
166}
167
168impl From<SmolStr> for CowStr<'_> {
169    #[inline]
170    fn from(s: SmolStr) -> Self {
171        CowStr::Owned(s)
172    }
173}
174
175impl From<CowStr<'_>> for Box<str> {
176    #[inline]
177    fn from(s: CowStr<'_>) -> Self {
178        match s {
179            CowStr::Borrowed(s) => s.into(),
180            CowStr::Owned(s) => String::from(s).into_boxed_str(),
181        }
182    }
183}
184
185impl<'a> PartialEq<CowStr<'a>> for CowStr<'_> {
186    #[inline]
187    fn eq(&self, other: &CowStr<'a>) -> bool {
188        self.deref() == other.deref()
189    }
190}
191
192impl PartialEq<&str> for CowStr<'_> {
193    #[inline]
194    fn eq(&self, other: &&str) -> bool {
195        self.deref() == *other
196    }
197}
198
199impl PartialEq<CowStr<'_>> for &str {
200    #[inline]
201    fn eq(&self, other: &CowStr<'_>) -> bool {
202        *self == other.deref()
203    }
204}
205
206impl PartialEq<String> for CowStr<'_> {
207    #[inline]
208    fn eq(&self, other: &String) -> bool {
209        self.deref() == other.as_str()
210    }
211}
212
213impl PartialEq<CowStr<'_>> for String {
214    #[inline]
215    fn eq(&self, other: &CowStr<'_>) -> bool {
216        self.as_str() == other.deref()
217    }
218}
219
220impl PartialOrd for CowStr<'_> {
221    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
222        Some(match (self, other) {
223            (CowStr::Borrowed(s1), CowStr::Borrowed(s2)) => s1.cmp(s2),
224            (CowStr::Borrowed(s1), CowStr::Owned(s2)) => s1.cmp(&s2.as_ref()),
225            (CowStr::Owned(s1), CowStr::Borrowed(s2)) => s1.as_str().cmp(s2),
226            (CowStr::Owned(s1), CowStr::Owned(s2)) => s1.cmp(s2),
227        })
228    }
229}
230
231impl Ord for CowStr<'_> {
232    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
233        match (self, other) {
234            (CowStr::Borrowed(s1), CowStr::Borrowed(s2)) => s1.cmp(s2),
235            (CowStr::Borrowed(s1), CowStr::Owned(s2)) => s1.cmp(&s2.as_ref()),
236            (CowStr::Owned(s1), CowStr::Borrowed(s2)) => s1.as_str().cmp(s2),
237            (CowStr::Owned(s1), CowStr::Owned(s2)) => s1.cmp(s2),
238        }
239    }
240}
241
242impl Eq for CowStr<'_> {}
243
244impl Hash for CowStr<'_> {
245    #[inline]
246    fn hash<H: Hasher>(&self, state: &mut H) {
247        self.deref().hash(state)
248    }
249}
250
251impl fmt::Debug for CowStr<'_> {
252    #[inline]
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        self.deref().fmt(f)
255    }
256}
257
258impl fmt::Display for CowStr<'_> {
259    #[inline]
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        self.deref().fmt(f)
262    }
263}
264
265impl IntoStatic for CowStr<'_> {
266    type Output = CowStr<'static>;
267
268    #[inline]
269    fn into_static(self) -> Self::Output {
270        match self {
271            CowStr::Borrowed(s) => CowStr::Owned((*s).into()),
272            CowStr::Owned(s) => CowStr::Owned(s),
273        }
274    }
275}
276
277impl Serialize for CowStr<'_> {
278    #[inline]
279    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280    where
281        S: serde::Serializer,
282    {
283        serializer.serialize_str(self)
284    }
285}
286
287/// Deserialization helper for things that wrap a CowStr
288pub struct CowStrVisitor;
289
290impl<'de> serde::de::Visitor<'de> for CowStrVisitor {
291    type Value = CowStr<'de>;
292
293    #[inline]
294    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
295        write!(formatter, "a string")
296    }
297
298    #[inline]
299    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
300    where
301        E: serde::de::Error,
302    {
303        Ok(CowStr::copy_from_str(v))
304    }
305
306    #[inline]
307    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
308    where
309        E: serde::de::Error,
310    {
311        Ok(CowStr::Borrowed(v))
312    }
313
314    #[inline]
315    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
316    where
317        E: serde::de::Error,
318    {
319        Ok(v.into())
320    }
321}
322
323impl<'de, 'a> Deserialize<'de> for CowStr<'a>
324where
325    'de: 'a,
326{
327    #[inline]
328    fn deserialize<D>(deserializer: D) -> Result<CowStr<'a>, D::Error>
329    where
330        D: serde::Deserializer<'de>,
331    {
332        deserializer.deserialize_str(CowStrVisitor)
333    }
334}
335
336/// Convert to a CowStr.
337pub trait ToCowStr {
338    /// Convert to a CowStr.
339    fn to_cowstr(&self) -> CowStr<'_>;
340}
341
342impl<T> ToCowStr for T
343where
344    T: fmt::Display + ?Sized,
345{
346    fn to_cowstr(&self) -> CowStr<'_> {
347        CowStr::Owned(smol_str::format_smolstr!("{}", self))
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_partialeq_with_str() {
357        let cow_str1 = CowStr::Borrowed("hello");
358        let cow_str2 = CowStr::Borrowed("hello");
359        let cow_str3 = CowStr::Borrowed("world");
360
361        assert_eq!(cow_str1, "hello");
362        assert_eq!("hello", cow_str1);
363        assert_eq!(cow_str1, cow_str2);
364        assert_ne!(cow_str1, "world");
365        assert_ne!("world", cow_str1);
366        assert_ne!(cow_str1, cow_str3);
367    }
368}