implicit_clone/
string.rs

1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::fmt::{self, Debug};
4use std::str::FromStr;
5
6use crate::ImplicitClone;
7
8use super::Rc;
9
10/// An immutable string type inspired by [Immutable.js](https://immutable-js.com/).
11///
12/// This type is cheap to clone and thus implements [`ImplicitClone`]. It can be created based on a
13/// `&'static str` or based on a reference counted string slice ([`str`]).
14#[derive(Debug, Clone)]
15pub enum IString {
16    /// A static string slice.
17    Static(&'static str),
18    /// A reference counted string slice.
19    Rc(Rc<str>),
20}
21
22impl IString {
23    /// Extracts a string slice containing the entire `IString`.
24    ///
25    /// # Examples
26    ///
27    /// Basic usage:
28    ///
29    /// ```
30    /// # use implicit_clone::unsync::IString;
31    /// let s = IString::from("foo");
32    ///
33    /// assert_eq!("foo", s.as_str());
34    /// ```
35    pub fn as_str(&self) -> &str {
36        match self {
37            Self::Static(s) => s,
38            Self::Rc(s) => s,
39        }
40    }
41
42    /// Obtain the contents of [`IString`] as a [`Cow`].
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// # use implicit_clone::unsync::IString;
48    /// use std::borrow::Cow;
49    /// let s = IString::from("foo");
50    ///
51    /// let cow: Cow<'_, str> = s.as_cow();
52    /// ```
53    pub fn as_cow(&self) -> Cow<'_, str> {
54        Cow::Borrowed(self.as_str())
55    }
56}
57
58impl Default for IString {
59    fn default() -> Self {
60        Self::Static("")
61    }
62}
63
64impl ImplicitClone for IString {}
65
66impl fmt::Display for IString {
67    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
68        fmt::Display::fmt(self.as_str(), f)
69    }
70}
71
72impl From<&str> for IString {
73    fn from(s: &str) -> IString {
74        IString::Rc(Rc::from(s))
75    }
76}
77
78impl From<String> for IString {
79    fn from(s: String) -> IString {
80        IString::Rc(Rc::from(s))
81    }
82}
83
84impl From<Rc<str>> for IString {
85    fn from(s: Rc<str>) -> IString {
86        IString::Rc(s)
87    }
88}
89
90impl From<Cow<'static, str>> for IString {
91    fn from(cow: Cow<'static, str>) -> Self {
92        match cow {
93            Cow::Borrowed(s) => IString::Static(s),
94            Cow::Owned(s) => s.into(),
95        }
96    }
97}
98
99impl From<std::fmt::Arguments<'_>> for IString {
100    fn from(args: std::fmt::Arguments) -> IString {
101        if let Some(s) = args.as_str() {
102            IString::Static(s)
103        } else {
104            IString::from(args.to_string())
105        }
106    }
107}
108
109impl From<&IString> for IString {
110    fn from(s: &IString) -> IString {
111        s.clone()
112    }
113}
114
115macro_rules! impl_cmp_as_str {
116    (PartialEq::<$type1:ty, $type2:ty>) => {
117        impl_cmp_as_str!(PartialEq::<$type1, $type2>::eq -> bool);
118    };
119    (PartialOrd::<$type1:ty, $type2:ty>) => {
120        impl_cmp_as_str!(PartialOrd::<$type1, $type2>::partial_cmp -> Option<Ordering>);
121    };
122    ($trait:ident :: <$type1:ty, $type2:ty> :: $fn:ident -> $ret:ty) => {
123        impl $trait<$type2> for $type1 {
124            fn $fn(&self, other: &$type2) -> $ret {
125                $trait::$fn(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
126            }
127        }
128    };
129}
130
131impl Eq for IString {}
132
133impl_cmp_as_str!(PartialEq::<IString, IString>);
134impl_cmp_as_str!(PartialEq::<IString, str>);
135impl_cmp_as_str!(PartialEq::<str, IString>);
136impl_cmp_as_str!(PartialEq::<IString, &str>);
137impl_cmp_as_str!(PartialEq::<&str, IString>);
138impl_cmp_as_str!(PartialEq::<IString, String>);
139impl_cmp_as_str!(PartialEq::<String, IString>);
140impl_cmp_as_str!(PartialEq::<IString, &String>);
141impl_cmp_as_str!(PartialEq::<&String, IString>);
142
143impl Ord for IString {
144    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
145        Ord::cmp(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
146    }
147}
148
149// Manual implementation of PartialOrd that uses Ord to ensure it is consistent, as
150// recommended by clippy.
151impl PartialOrd for IString {
152    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
153        Some(self.cmp(other))
154    }
155}
156
157impl_cmp_as_str!(PartialOrd::<IString, str>);
158impl_cmp_as_str!(PartialOrd::<str, IString>);
159impl_cmp_as_str!(PartialOrd::<IString, &str>);
160impl_cmp_as_str!(PartialOrd::<&str, IString>);
161impl_cmp_as_str!(PartialOrd::<IString, String>);
162impl_cmp_as_str!(PartialOrd::<String, IString>);
163impl_cmp_as_str!(PartialOrd::<IString, &String>);
164impl_cmp_as_str!(PartialOrd::<&String, IString>);
165
166impl std::ops::Deref for IString {
167    type Target = str;
168
169    fn deref(&self) -> &Self::Target {
170        self.as_str()
171    }
172}
173
174impl AsRef<str> for IString {
175    fn as_ref(&self) -> &str {
176        self.as_str()
177    }
178}
179
180impl std::hash::Hash for IString {
181    #[inline]
182    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
183        std::hash::Hash::hash(self.as_str(), state)
184    }
185}
186
187impl std::borrow::Borrow<str> for IString {
188    fn borrow(&self) -> &str {
189        self.as_str()
190    }
191}
192
193impl FromStr for IString {
194    type Err = std::convert::Infallible;
195    fn from_str(value: &str) -> Result<Self, Self::Err> {
196        Ok(IString::from(value))
197    }
198}
199
200#[cfg(feature = "serde")]
201impl serde::Serialize for IString {
202    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
203        <str as serde::Serialize>::serialize(self, serializer)
204    }
205}
206
207#[cfg(feature = "serde")]
208impl<'de> serde::Deserialize<'de> for IString {
209    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210        <String as serde::Deserialize>::deserialize(deserializer).map(IString::from)
211    }
212}
213
214#[cfg(test)]
215mod test_string {
216    use super::*;
217
218    //
219    // Frames wrap a value with a particular syntax
220    // that may not be easy to write with plain macro_rules arg types
221    //
222
223    macro_rules! frame_i_static {
224        ($a:expr) => {
225            IString::Static($a)
226        };
227    }
228
229    macro_rules! frame_i_rc {
230        ($a:expr) => {
231            IString::Rc(Rc::from($a))
232        };
233    }
234
235    macro_rules! frame_deref {
236        ($a:expr) => {
237            *$a
238        };
239    }
240
241    macro_rules! frame_noop {
242        ($a:expr) => {
243            $a
244        };
245    }
246
247    macro_rules! frame_string {
248        ($a:expr) => {
249            String::from($a)
250        };
251    }
252
253    macro_rules! frame_string_ref {
254        ($a:expr) => {
255            &String::from($a)
256        };
257    }
258
259    #[test]
260    fn eq_ne_self() {
261        macro_rules! test_one {
262            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
263                $macro!($frame1!($a), $frame2!($b));
264            };
265        }
266
267        macro_rules! test_all_frame_combos {
268            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
269                // 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
270                test_one!($macro!, $frame1!, $frame1!, $a, $b);
271                test_one!($macro!, $frame2!, $frame1!, $a, $b);
272                test_one!($macro!, $frame1!, $frame2!, $a, $b);
273                test_one!($macro!, $frame2!, $frame2!, $a, $b);
274            };
275            ($macro:tt!, $a:literal, $b:literal) => {
276                test_all_frame_combos!($macro!, frame_i_static!, frame_i_rc!, $a, $b);
277            };
278        }
279
280        test_all_frame_combos!(assert_eq!, "foo", "foo");
281        test_all_frame_combos!(assert_ne!, "foo", "bar");
282    }
283
284    #[test]
285    fn cmp_self() {
286        macro_rules! test_one {
287            ($res:expr, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
288                assert_eq!($res, Ord::cmp(&$frame1!($a), &$frame2!($b)));
289            };
290        }
291
292        macro_rules! test_all_frame_combos {
293            ($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
294                // 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
295                test_one!($res, $frame1!, $frame1!, $a, $b);
296                test_one!($res, $frame2!, $frame1!, $a, $b);
297                test_one!($res, $frame1!, $frame2!, $a, $b);
298                test_one!($res, $frame2!, $frame2!, $a, $b);
299            };
300            ($res:expr, $a:literal, $b:literal) => {
301                test_all_frame_combos!($res, frame_i_static!, frame_i_rc!, $a, $b);
302            };
303        }
304
305        test_all_frame_combos!(Ordering::Equal, "foo", "foo");
306        test_all_frame_combos!(Ordering::Greater, "foo", "bar");
307        test_all_frame_combos!(Ordering::Less, "bar", "foo");
308        test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
309        test_all_frame_combos!(Ordering::Less, "foo", "foobar");
310    }
311
312    #[test]
313    fn eq_ne_strings() {
314        macro_rules! test_one {
315            ($macro:tt!, $a:expr, $b:expr) => {
316                $macro!($a, $b);
317                $macro!($b, $a);
318            };
319        }
320
321        macro_rules! test_all_frame_combos {
322            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
323                // 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
324                test_one!($macro!, $frame1!($a), $frame2!($b));
325                test_one!($macro!, $frame2!($a), $frame1!($b));
326            };
327            ($macro:tt!, $frame2:tt!, $a:literal, $b:literal) => {
328                test_all_frame_combos!($macro!, frame_i_rc!, $frame2!, $a, $b);
329                test_all_frame_combos!($macro!, frame_i_static!, $frame2!, $a, $b);
330            };
331            ($macro:tt!, $a:literal, $b:literal) => {
332                test_all_frame_combos!($macro!, frame_deref!, $a, $b);
333                test_all_frame_combos!($macro!, frame_noop!, $a, $b);
334                test_all_frame_combos!($macro!, frame_string!, $a, $b);
335                test_all_frame_combos!($macro!, frame_string_ref!, $a, $b);
336            };
337        }
338
339        test_all_frame_combos!(assert_eq!, "foo", "foo");
340        test_all_frame_combos!(assert_ne!, "foo", "bar");
341    }
342
343    #[test]
344    fn partial_cmp_strings() {
345        macro_rules! test_one {
346            ($res:expr, $a:expr, $b:expr) => {
347                assert_eq!(Some($res), PartialOrd::partial_cmp(&$a, &$b));
348            };
349        }
350
351        macro_rules! test_all_frame_combos {
352            ($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
353                // 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
354                test_one!($res, $frame1!($a), $frame2!($b));
355                test_one!($res, $frame2!($a), $frame1!($b));
356            };
357            ($res:expr, $frame2:tt!, $a:literal, $b:literal) => {
358                test_all_frame_combos!($res, frame_i_rc!, $frame2!, $a, $b);
359                test_all_frame_combos!($res, frame_i_static!, $frame2!, $a, $b);
360            };
361            ($res:expr, $a:literal, $b:literal) => {
362                test_all_frame_combos!($res, frame_deref!, $a, $b);
363                test_all_frame_combos!($res, frame_noop!, $a, $b);
364                test_all_frame_combos!($res, frame_string!, $a, $b);
365                test_all_frame_combos!($res, frame_string_ref!, $a, $b);
366            };
367        }
368
369        test_all_frame_combos!(Ordering::Equal, "foo", "foo");
370        test_all_frame_combos!(Ordering::Greater, "foo", "bar");
371        test_all_frame_combos!(Ordering::Less, "bar", "foo");
372        test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
373        test_all_frame_combos!(Ordering::Less, "foo", "foobar");
374    }
375
376    #[test]
377    fn const_string() {
378        const _STRING: IString = IString::Static("foo");
379    }
380
381    #[test]
382    fn deref_str() {
383        assert_eq!(IString::Static("foo").to_uppercase(), "FOO");
384        assert_eq!(IString::Rc(Rc::from("foo")).to_uppercase(), "FOO");
385    }
386
387    #[test]
388    fn borrow_str() {
389        let map: std::collections::HashMap<_, _> = [
390            (IString::Static("foo"), true),
391            (IString::Rc(Rc::from("bar")), true),
392        ]
393        .into_iter()
394        .collect();
395
396        assert_eq!(map.get("foo").copied(), Some(true));
397        assert_eq!(map.get("bar").copied(), Some(true));
398    }
399
400    #[test]
401    fn as_cow_does_not_clone() {
402        let rc_s = Rc::from("foo");
403
404        let s = IString::Rc(Rc::clone(&rc_s));
405        assert_eq!(Rc::strong_count(&rc_s), 2);
406
407        let cow: Cow<'_, str> = s.as_cow();
408        assert_eq!(Rc::strong_count(&rc_s), 2);
409
410        // this assert exists to ensure the cow lives after the strong_count assert
411        assert_eq!(cow, "foo");
412    }
413
414    #[test]
415    fn from_ref() {
416        let s = IString::Static("foo");
417        let _out = IString::from(&s);
418    }
419
420    #[test]
421    fn from_fmt_arguments() {
422        let s = IString::from(format_args!("Hello World!"));
423        assert!(matches!(s, IString::Static("Hello World!")));
424
425        let name = "Jane";
426        let s = IString::from(format_args!("Hello {name}!"));
427        assert!(matches!(s, IString::Rc(_)));
428        assert_eq!(s, "Hello Jane!");
429    }
430}