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<&'static str> for IString {
73    fn from(s: &'static str) -> IString {
74        IString::Static(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<&IString> for IString {
100    fn from(s: &IString) -> IString {
101        s.clone()
102    }
103}
104
105macro_rules! impl_cmp_as_str {
106    (PartialEq::<$type1:ty, $type2:ty>) => {
107        impl_cmp_as_str!(PartialEq::<$type1, $type2>::eq -> bool);
108    };
109    (PartialOrd::<$type1:ty, $type2:ty>) => {
110        impl_cmp_as_str!(PartialOrd::<$type1, $type2>::partial_cmp -> Option<Ordering>);
111    };
112    ($trait:ident :: <$type1:ty, $type2:ty> :: $fn:ident -> $ret:ty) => {
113        impl $trait<$type2> for $type1 {
114            fn $fn(&self, other: &$type2) -> $ret {
115                $trait::$fn(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
116            }
117        }
118    };
119}
120
121impl Eq for IString {}
122
123impl_cmp_as_str!(PartialEq::<IString, IString>);
124impl_cmp_as_str!(PartialEq::<IString, str>);
125impl_cmp_as_str!(PartialEq::<str, IString>);
126impl_cmp_as_str!(PartialEq::<IString, &str>);
127impl_cmp_as_str!(PartialEq::<&str, IString>);
128impl_cmp_as_str!(PartialEq::<IString, String>);
129impl_cmp_as_str!(PartialEq::<String, IString>);
130impl_cmp_as_str!(PartialEq::<IString, &String>);
131impl_cmp_as_str!(PartialEq::<&String, IString>);
132
133impl Ord for IString {
134    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
135        Ord::cmp(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
136    }
137}
138
139// Manual implementation of PartialOrd that uses Ord to ensure it is consistent, as
140// recommended by clippy.
141impl PartialOrd for IString {
142    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
143        Some(self.cmp(other))
144    }
145}
146
147impl_cmp_as_str!(PartialOrd::<IString, str>);
148impl_cmp_as_str!(PartialOrd::<str, IString>);
149impl_cmp_as_str!(PartialOrd::<IString, &str>);
150impl_cmp_as_str!(PartialOrd::<&str, IString>);
151impl_cmp_as_str!(PartialOrd::<IString, String>);
152impl_cmp_as_str!(PartialOrd::<String, IString>);
153impl_cmp_as_str!(PartialOrd::<IString, &String>);
154impl_cmp_as_str!(PartialOrd::<&String, IString>);
155
156impl std::ops::Deref for IString {
157    type Target = str;
158
159    fn deref(&self) -> &Self::Target {
160        self.as_str()
161    }
162}
163
164impl AsRef<str> for IString {
165    fn as_ref(&self) -> &str {
166        self.as_str()
167    }
168}
169
170impl std::hash::Hash for IString {
171    #[inline]
172    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
173        std::hash::Hash::hash(self.as_str(), state)
174    }
175}
176
177impl std::borrow::Borrow<str> for IString {
178    fn borrow(&self) -> &str {
179        self.as_str()
180    }
181}
182
183impl FromStr for IString {
184    type Err = std::convert::Infallible;
185    fn from_str(value: &str) -> Result<Self, Self::Err> {
186        Ok(IString::from(String::from(value)))
187    }
188}
189
190#[cfg(feature = "serde")]
191impl serde::Serialize for IString {
192    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
193        <str as serde::Serialize>::serialize(self, serializer)
194    }
195}
196
197#[cfg(feature = "serde")]
198impl<'de> serde::Deserialize<'de> for IString {
199    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
200        <String as serde::Deserialize>::deserialize(deserializer).map(IString::from)
201    }
202}
203
204#[cfg(test)]
205mod test_string {
206    use super::*;
207
208    //
209    // Frames wrap a value with a particular syntax
210    // that may not be easy to write with plain macro_rules arg types
211    //
212
213    macro_rules! frame_i_static {
214        ($a:expr) => {
215            IString::Static($a)
216        };
217    }
218
219    macro_rules! frame_i_rc {
220        ($a:expr) => {
221            IString::Rc(Rc::from($a))
222        };
223    }
224
225    macro_rules! frame_deref {
226        ($a:expr) => {
227            *$a
228        };
229    }
230
231    macro_rules! frame_noop {
232        ($a:expr) => {
233            $a
234        };
235    }
236
237    macro_rules! frame_string {
238        ($a:expr) => {
239            String::from($a)
240        };
241    }
242
243    macro_rules! frame_string_ref {
244        ($a:expr) => {
245            &String::from($a)
246        };
247    }
248
249    #[test]
250    fn eq_ne_self() {
251        macro_rules! test_one {
252            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
253                $macro!($frame1!($a), $frame2!($b));
254            };
255        }
256
257        macro_rules! test_all_frame_combos {
258            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
259                // 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
260                test_one!($macro!, $frame1!, $frame1!, $a, $b);
261                test_one!($macro!, $frame2!, $frame1!, $a, $b);
262                test_one!($macro!, $frame1!, $frame2!, $a, $b);
263                test_one!($macro!, $frame2!, $frame2!, $a, $b);
264            };
265            ($macro:tt!, $a:literal, $b:literal) => {
266                test_all_frame_combos!($macro!, frame_i_static!, frame_i_rc!, $a, $b);
267            };
268        }
269
270        test_all_frame_combos!(assert_eq!, "foo", "foo");
271        test_all_frame_combos!(assert_ne!, "foo", "bar");
272    }
273
274    #[test]
275    fn cmp_self() {
276        macro_rules! test_one {
277            ($res:expr, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
278                assert_eq!($res, Ord::cmp(&$frame1!($a), &$frame2!($b)));
279            };
280        }
281
282        macro_rules! test_all_frame_combos {
283            ($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
284                // 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
285                test_one!($res, $frame1!, $frame1!, $a, $b);
286                test_one!($res, $frame2!, $frame1!, $a, $b);
287                test_one!($res, $frame1!, $frame2!, $a, $b);
288                test_one!($res, $frame2!, $frame2!, $a, $b);
289            };
290            ($res:expr, $a:literal, $b:literal) => {
291                test_all_frame_combos!($res, frame_i_static!, frame_i_rc!, $a, $b);
292            };
293        }
294
295        test_all_frame_combos!(Ordering::Equal, "foo", "foo");
296        test_all_frame_combos!(Ordering::Greater, "foo", "bar");
297        test_all_frame_combos!(Ordering::Less, "bar", "foo");
298        test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
299        test_all_frame_combos!(Ordering::Less, "foo", "foobar");
300    }
301
302    #[test]
303    fn eq_ne_strings() {
304        macro_rules! test_one {
305            ($macro:tt!, $a:expr, $b:expr) => {
306                $macro!($a, $b);
307                $macro!($b, $a);
308            };
309        }
310
311        macro_rules! test_all_frame_combos {
312            ($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
313                // 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
314                test_one!($macro!, $frame1!($a), $frame2!($b));
315                test_one!($macro!, $frame2!($a), $frame1!($b));
316            };
317            ($macro:tt!, $frame2:tt!, $a:literal, $b:literal) => {
318                test_all_frame_combos!($macro!, frame_i_rc!, $frame2!, $a, $b);
319                test_all_frame_combos!($macro!, frame_i_static!, $frame2!, $a, $b);
320            };
321            ($macro:tt!, $a:literal, $b:literal) => {
322                test_all_frame_combos!($macro!, frame_deref!, $a, $b);
323                test_all_frame_combos!($macro!, frame_noop!, $a, $b);
324                test_all_frame_combos!($macro!, frame_string!, $a, $b);
325                test_all_frame_combos!($macro!, frame_string_ref!, $a, $b);
326            };
327        }
328
329        test_all_frame_combos!(assert_eq!, "foo", "foo");
330        test_all_frame_combos!(assert_ne!, "foo", "bar");
331    }
332
333    #[test]
334    fn partial_cmp_strings() {
335        macro_rules! test_one {
336            ($res:expr, $a:expr, $b:expr) => {
337                assert_eq!(Some($res), PartialOrd::partial_cmp(&$a, &$b));
338            };
339        }
340
341        macro_rules! test_all_frame_combos {
342            ($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
343                // 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
344                test_one!($res, $frame1!($a), $frame2!($b));
345                test_one!($res, $frame2!($a), $frame1!($b));
346            };
347            ($res:expr, $frame2:tt!, $a:literal, $b:literal) => {
348                test_all_frame_combos!($res, frame_i_rc!, $frame2!, $a, $b);
349                test_all_frame_combos!($res, frame_i_static!, $frame2!, $a, $b);
350            };
351            ($res:expr, $a:literal, $b:literal) => {
352                test_all_frame_combos!($res, frame_deref!, $a, $b);
353                test_all_frame_combos!($res, frame_noop!, $a, $b);
354                test_all_frame_combos!($res, frame_string!, $a, $b);
355                test_all_frame_combos!($res, frame_string_ref!, $a, $b);
356            };
357        }
358
359        test_all_frame_combos!(Ordering::Equal, "foo", "foo");
360        test_all_frame_combos!(Ordering::Greater, "foo", "bar");
361        test_all_frame_combos!(Ordering::Less, "bar", "foo");
362        test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
363        test_all_frame_combos!(Ordering::Less, "foo", "foobar");
364    }
365
366    #[test]
367    fn const_string() {
368        const _STRING: IString = IString::Static("foo");
369    }
370
371    #[test]
372    fn deref_str() {
373        assert_eq!(IString::Static("foo").to_uppercase(), "FOO");
374        assert_eq!(IString::Rc(Rc::from("foo")).to_uppercase(), "FOO");
375    }
376
377    #[test]
378    fn borrow_str() {
379        let map: std::collections::HashMap<_, _> = [
380            (IString::Static("foo"), true),
381            (IString::Rc(Rc::from("bar")), true),
382        ]
383        .into_iter()
384        .collect();
385
386        assert_eq!(map.get("foo").copied(), Some(true));
387        assert_eq!(map.get("bar").copied(), Some(true));
388    }
389
390    #[test]
391    fn as_cow_does_not_clone() {
392        let rc_s = Rc::from("foo");
393
394        let s = IString::Rc(Rc::clone(&rc_s));
395        assert_eq!(Rc::strong_count(&rc_s), 2);
396
397        let cow: Cow<'_, str> = s.as_cow();
398        assert_eq!(Rc::strong_count(&rc_s), 2);
399
400        // this assert exists to ensure the cow lives after the strong_count assert
401        assert_eq!(cow, "foo");
402    }
403
404    #[test]
405    fn from_ref() {
406        let s = IString::Static("foo");
407        let _out = IString::from(&s);
408    }
409}