Skip to content

Commit

Permalink
Add impl for PartialOrd, Ord, reverse PartialEq (#27)
Browse files Browse the repository at this point in the history
This PR implements `PartialOrd` and `Ord` for `IString`, fixes #18,
additionally adds more implementations for reverse comparisons (`str =
IString`, as opposed to usual `IString = str`).

It adds an `impl_cmp_as_str!` macro which allows to list implementations
easily by mentioning the trait name and pairs for which to implement the
trait.

It also updates tests using macros to list more possible usage variants
(comparing `IString`s of different `enum` variants for all tests,
reverse comparisons, adding tests for `PartialOrd` and `Ord`).
  • Loading branch information
kirillsemyonkin authored Aug 10, 2023
1 parent cef5246 commit d3bb992
Showing 1 changed file with 202 additions and 42 deletions.
244 changes: 202 additions & 42 deletions src/string.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::str::FromStr;

/// An immutable string type inspired by [Immutable.js](https://immutable-js.com/).
///
/// This type is cheap to clone and thus implements [`ImplicitClone`]. It can be created based on a
/// `&'static str` or based on a reference counted string slice ([`str`]).
#[derive(Debug, Clone, Eq)]
#[derive(Debug, Clone)]
pub enum IString {
/// A static string slice.
Static(&'static str),
Expand Down Expand Up @@ -91,35 +92,49 @@ impl From<Cow<'static, str>> for IString {
}
}

impl PartialEq<IString> for IString {
fn eq(&self, other: &IString) -> bool {
self.as_str().eq(other.as_str())
}
}

impl PartialEq<str> for IString {
fn eq(&self, other: &str) -> bool {
self.as_str().eq(other)
}
}

impl PartialEq<&str> for IString {
fn eq(&self, other: &&str) -> bool {
self.eq(*other)
}
macro_rules! impl_cmp_as_str {
(PartialEq::<$type1:ty, $type2:ty>) => {
impl_cmp_as_str!(PartialEq::<$type1, $type2>::eq -> bool);
};
(PartialOrd::<$type1:ty, $type2:ty>) => {
impl_cmp_as_str!(PartialOrd::<$type1, $type2>::partial_cmp -> Option<Ordering>);
};
($trait:ident :: <$type1:ty, $type2:ty> :: $fn:ident -> $ret:ty) => {
impl $trait<$type2> for $type1 {
fn $fn(&self, other: &$type2) -> $ret {
$trait::$fn(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
}
}
};
}

impl PartialEq<String> for IString {
fn eq(&self, other: &String) -> bool {
self.eq(other.as_str())
impl Eq for IString {}

impl_cmp_as_str!(PartialEq::<IString, IString>);
impl_cmp_as_str!(PartialEq::<IString, str>);
impl_cmp_as_str!(PartialEq::<str, IString>);
impl_cmp_as_str!(PartialEq::<IString, &str>);
impl_cmp_as_str!(PartialEq::<&str, IString>);
impl_cmp_as_str!(PartialEq::<IString, String>);
impl_cmp_as_str!(PartialEq::<String, IString>);
impl_cmp_as_str!(PartialEq::<IString, &String>);
impl_cmp_as_str!(PartialEq::<&String, IString>);

impl Ord for IString {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
Ord::cmp(AsRef::<str>::as_ref(self), AsRef::<str>::as_ref(other))
}
}

impl PartialEq<&String> for IString {
fn eq(&self, other: &&String) -> bool {
self.eq(*other)
}
}
impl_cmp_as_str!(PartialOrd::<IString, IString>);
impl_cmp_as_str!(PartialOrd::<IString, str>);
impl_cmp_as_str!(PartialOrd::<str, IString>);
impl_cmp_as_str!(PartialOrd::<IString, &str>);
impl_cmp_as_str!(PartialOrd::<&str, IString>);
impl_cmp_as_str!(PartialOrd::<IString, String>);
impl_cmp_as_str!(PartialOrd::<String, IString>);
impl_cmp_as_str!(PartialOrd::<IString, &String>);
impl_cmp_as_str!(PartialOrd::<&String, IString>);

impl std::ops::Deref for IString {
type Target = str;
Expand Down Expand Up @@ -163,7 +178,7 @@ impl serde::Serialize for IString {
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for IString{
impl<'de> serde::Deserialize<'de> for IString {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
<String as serde::Deserialize>::deserialize(deserializer).map(IString::from)
}
Expand All @@ -173,40 +188,183 @@ impl<'de> serde::Deserialize<'de> for IString{
mod test_string {
use super::*;

//
// Frames wrap a value with a particular syntax
// that may not be easy to write with plain macro_rules arg types
//

macro_rules! frame_i_static {
($a:expr) => {
IString::Static($a)
};
}

macro_rules! frame_i_rc {
($a:expr) => {
IString::Rc(Rc::from($a))
};
}

macro_rules! frame_deref {
($a:expr) => {
*$a
};
}

macro_rules! frame_noop {
($a:expr) => {
$a
};
}

macro_rules! frame_string {
($a:expr) => {
String::from($a)
};
}

macro_rules! frame_string_ref {
($a:expr) => {
&String::from($a)
};
}

#[test]
fn eq_ne_self() {
macro_rules! test_one {
($macro:tt!, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
$macro!($frame1!($a), $frame2!($b));
};
}

macro_rules! test_all_frame_combos {
($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
// 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
test_one!($macro!, $frame1!, $frame1!, $a, $b);
test_one!($macro!, $frame2!, $frame1!, $a, $b);
test_one!($macro!, $frame1!, $frame2!, $a, $b);
test_one!($macro!, $frame2!, $frame2!, $a, $b);
};
($macro:tt!, $a:literal, $b:literal) => {
test_all_frame_combos!($macro!, frame_i_static!, frame_i_rc!, $a, $b);
};
}

test_all_frame_combos!(assert_eq!, "foo", "foo");
test_all_frame_combos!(assert_ne!, "foo", "bar");
}

#[test]
fn cmp_self() {
macro_rules! test_one {
($res:expr, $frame1:tt!, $frame2:tt!, $a:expr, $b:expr) => {
assert_eq!($res, Ord::cmp(&$frame1!($a), &$frame2!($b)));
};
}

macro_rules! test_all_frame_combos {
($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
// 11, 12, 21, 22 - i_static-i_static, i_static-i_rc, ...
test_one!($res, $frame1!, $frame1!, $a, $b);
test_one!($res, $frame2!, $frame1!, $a, $b);
test_one!($res, $frame1!, $frame2!, $a, $b);
test_one!($res, $frame2!, $frame2!, $a, $b);
};
($res:expr, $a:literal, $b:literal) => {
test_all_frame_combos!($res, frame_i_static!, frame_i_rc!, $a, $b);
};
}

test_all_frame_combos!(Ordering::Equal, "foo", "foo");
test_all_frame_combos!(Ordering::Greater, "foo", "bar");
test_all_frame_combos!(Ordering::Less, "bar", "foo");
test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
test_all_frame_combos!(Ordering::Less, "foo", "foobar");
}

#[test]
fn cmp() {
assert_eq!(IString::Static("foo"), IString::Static("foo"));
assert_eq!(IString::Static("foo"), IString::Rc(Rc::from("foo")));
assert_eq!(IString::Rc(Rc::from("foo")), IString::Rc(Rc::from("foo")));
fn eq_ne_strings() {
macro_rules! test_one {
($macro:tt!, $a:expr, $b:expr) => {
$macro!($a, $b);
$macro!($b, $a);
};
}

macro_rules! test_all_frame_combos {
($macro:tt!, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
// 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
test_one!($macro!, $frame1!($a), $frame2!($b));
test_one!($macro!, $frame2!($a), $frame1!($b));
};
($macro:tt!, $frame2:tt!, $a:literal, $b:literal) => {
test_all_frame_combos!($macro!, frame_i_rc!, $frame2!, $a, $b);
test_all_frame_combos!($macro!, frame_i_static!, $frame2!, $a, $b);
};
($macro:tt!, $a:literal, $b:literal) => {
test_all_frame_combos!($macro!, frame_deref!, $a, $b);
test_all_frame_combos!($macro!, frame_noop!, $a, $b);
test_all_frame_combos!($macro!, frame_string!, $a, $b);
test_all_frame_combos!($macro!, frame_string_ref!, $a, $b);
};
}

assert_ne!(IString::Static("foo"), IString::Static("bar"));
assert_ne!(IString::Static("foo"), IString::Rc(Rc::from("bar")));
assert_ne!(IString::Rc(Rc::from("foo")), IString::Rc(Rc::from("bar")));
test_all_frame_combos!(assert_eq!, "foo", "foo");
test_all_frame_combos!(assert_ne!, "foo", "bar");
}

#[test]
fn string_cmp() {
assert_eq!(IString::from("foo"), "foo");
assert_eq!(IString::from("foo"), String::from("foo"));
assert_eq!(IString::from("foo"), &String::from("foo"));
fn partial_cmp_strings() {
macro_rules! test_one {
($res:expr, $a:expr, $b:expr) => {
assert_eq!(Some($res), PartialOrd::partial_cmp(&$a, &$b));
};
}

macro_rules! test_all_frame_combos {
($res:expr, $frame1:tt!, $frame2:tt!, $a:literal, $b:literal) => {
// 12, 21 - i_rc-deref, deref-i_rc, ..., static-string_ref, string_ref-static
test_one!($res, $frame1!($a), $frame2!($b));
test_one!($res, $frame2!($a), $frame1!($b));
};
($res:expr, $frame2:tt!, $a:literal, $b:literal) => {
test_all_frame_combos!($res, frame_i_rc!, $frame2!, $a, $b);
test_all_frame_combos!($res, frame_i_static!, $frame2!, $a, $b);
};
($res:expr, $a:literal, $b:literal) => {
test_all_frame_combos!($res, frame_deref!, $a, $b);
test_all_frame_combos!($res, frame_noop!, $a, $b);
test_all_frame_combos!($res, frame_string!, $a, $b);
test_all_frame_combos!($res, frame_string_ref!, $a, $b);
};
}

test_all_frame_combos!(Ordering::Equal, "foo", "foo");
test_all_frame_combos!(Ordering::Greater, "foo", "bar");
test_all_frame_combos!(Ordering::Less, "bar", "foo");
test_all_frame_combos!(Ordering::Greater, "foobar", "foo");
test_all_frame_combos!(Ordering::Less, "foo", "foobar");
}

#[test]
fn static_string() {
fn const_string() {
const _STRING: IString = IString::Static("foo");
}

#[test]
fn deref_str() {
assert_eq!(IString::Static("foo").to_uppercase(), "FOO");
assert_eq!(IString::Rc(Rc::from("foo")).to_uppercase(), "FOO");
}

#[test]
fn borrow_str() {
let map: std::collections::HashMap<_, _> = [
(IString::Static("foo"), true),
(IString::Rc(Rc::from("bar")), true)
].into_iter().collect();
(IString::Rc(Rc::from("bar")), true),
]
.into_iter()
.collect();

assert_eq!(map.get("foo").copied(), Some(true));
assert_eq!(map.get("bar").copied(), Some(true));
Expand All @@ -215,12 +373,14 @@ mod test_string {
#[test]
fn as_cow_does_not_clone() {
let rc_s = Rc::from("foo");

let s = IString::Rc(Rc::clone(&rc_s));
assert_eq!(Rc::strong_count(&rc_s), 2);

let cow: Cow<'_, str> = s.as_cow();
assert_eq!(Rc::strong_count(&rc_s), 2);
// this assert exists to ensure the cow lives after the strong_count asset
assert_eq!(cow, "foo");

// this assert exists to ensure the cow lives after the strong_count assert
assert_eq!(cow, "foo");
}
}

0 comments on commit d3bb992

Please sign in to comment.