string-join 0.1.2

A python-like way to join items in an iterator with a separator
Documentation
//! String join
//!
//! Uses a python-like syntax to join any iterator-like thing with any string-like thing
//!
//! # Example
//!
//! ```rust
//! use string_join::Join;
//!
//! assert_eq!("=".join(&["a", "b", "c"]), String::from("a=b=c"));
//! ```
//!
//! You can also write a joined collection directly to an `io::Write`r
//!
//! ```rust,no_run
//! use std::fs::File;
//! use string_join::display::Join;
//!
//! # fn main() -> std::io::Result<()> {
//! let mut f = File::create("foo.txt")?;
//! "\n".write_join(&mut f, &['a', 'b', 'c'])?; // => writes `a\nb\nc` to the writer and returns
//!                                             //    the number of bytes written (5, in this case)
//! #   Ok(())
//! # }
//! ```
//!
//! # `AsRef<str>` vs `Display`
//!
//! There are two ways to convert items in a collection to a joined string. If all the items, as
//! well as the separator, implement `AsRef<str>`, then we only have to allocate a single string
//! and can write bytes into it. However, this limits the types that we can join. If we instead
//! limit the types we can join to types that implement `Display`, we can join a lot more types,
//! with the tradeoff that a lot more allocations will be happening.
//!
//! Therefore, there are two modules in this crate, `string_join::as_ref` and
//! `string_join::display`. They both provide a trait called `Join` and both the traits have
//! methods `write_join` and `join`. However, one is implemented for `AsRef<str>` types and one for
//! `Display` types. This way, the user of the crate can decide which implementation they want to
//! use.
//!
//! To keep backwards compatibility, `string_join::Join` is the same as
//! `string_join::as_ref::Join`.

pub use crate::as_ref::Join;

/// A `Join` implementation for iterator item types that implement `Display`
pub mod display {
    /// This trait brings the `join` and `write_join` methods into scope.
    pub trait Join {

        /// Called on the separator, this takes a writer and something that can be turned into an
        /// iterator and writes the joined result to the writer.
        fn write_join<I: Display, W: Write, T: IntoIterator<Item=I>>(&self, writer: W, coll: T) -> io::Result<usize>;

        /// Called on the separator, this takes something that can be turned into an iterator and
        /// produces a heap-allocated string
        fn join<I: Display, T: IntoIterator<Item=I>>(&self, coll: T) -> String {
            let mut s = Vec::new();
            // Safety: will only panic if we OOM, in which case we're screwed anyway
            self.write_join(&mut s, coll).expect("This shouldn't fail");
            // Safety: all inputs to write_join are `AsRef<str>` so they will all be valid utf-8 and
            // therefore this can't fail
            String::from_utf8(s).unwrap()
        }
    }

    impl<A: AsRef<str>> Join for A {
        fn write_join<I: Display, W: Write, T: IntoIterator<Item=I>>(&self, mut writer: W, coll: T) -> io::Result<usize> {
            let mut iter = coll.into_iter();
            let mut written = 0;
            if let Some(first) = iter.next() {
                let first = first.to_string();
                let bytes = first.as_bytes();
                writer.write_all(bytes)?;
                written += bytes.len();
            }
            for s in iter {
                let t1 = self.as_ref().as_bytes();
                writer.write_all(t1)?;
                written += t1.len();

                let s = s.to_string();
                let t2 = s.as_bytes();
                writer.write_all(t2)?;
                written += t2.len();
            }
            writer.flush()?;
            io::Result::Ok(written)
        }
    }

    #[cfg(test)]
    mod tests {
        #[test]
        fn write_join_to_file() {
            let v = vec![];
            let mut cur = Cursor::new(v);
            let written = "=".write_join(&mut cur, &['a', 'b', 'c']).expect("Couldn't write joined string");
            let result = cur.into_inner();
            assert_eq!(written, 5);
            assert_eq!(&result[..], "a=b=c".as_bytes());
        }

        #[test]
        fn join_with_heap_string() {
            let equal = String::from("=");
            let result = equal.join(&['a', 'b', 'c']);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_with_borrowed_string() {
            let equal = String::from("=");
            let eq = &equal[..];
            let result = eq.join(&['a', 'b', 'c']);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_with_cow_string() {
            let equal = String::from("=");
            let eq: Cow<'_, str> = Cow::Borrowed(&equal[..]);
            let result = eq.join(&['a', 'b', 'c']);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_hashset() {
            let set = {
                let mut set = HashSet::new();
                set.insert('a');
                set.insert('b');
                set.insert('c');
                set
            };
            let result = "=".join(&set);
            assert!(vec!["a=b=c", "a=c=b", "b=a=c", "b=c=a", "c=a=b", "c=b=a"].contains(&&result[..]));
        }

        use std::io::Cursor;
        use std::borrow::Cow;
        use std::collections::HashSet;
        use super::Join;
    }

    use std::{
        fmt::Display,
        io::{self, Write},
    };
}

/// A `Join` implementation for iterator item types that implement `AsRef<str>`
pub mod as_ref {

    /// This trait brings the `join` and `write_join` methods into scope.
    pub trait Join {

        /// Called on the separator, this takes a writer and something that can be turned into an
        /// iterator and writes the joined result to the writer.
        fn write_join<I: AsRef<str>, W: Write, T: IntoIterator<Item=I>>(&self, writer: W, coll: T) -> io::Result<usize>;

        /// Called on the separator, this takes something that can be turned into an iterator and
        /// produces a heap-allocated string
        fn join<I: AsRef<str>, T: IntoIterator<Item=I>>(&self, coll: T) -> String {
            let mut s = Vec::new();
            // Safety: will only panic if we OOM, in which case we're screwed anyway
            self.write_join(&mut s, coll).expect("This shouldn't fail");
            // Safety: all inputs to write_join are `AsRef<str>` so they will all be valid utf-8 and
            // therefore this can't fail
            String::from_utf8(s).unwrap()
        }
    }

    impl<A: AsRef<str>> Join for A {
        fn write_join<I: AsRef<str>, W: Write, T: IntoIterator<Item=I>>(&self, mut writer: W, coll: T) -> io::Result<usize> {
            let mut iter = coll.into_iter();
            let mut written = 0;
            if let Some(first) = iter.next() {
                let bytes = first.as_ref().as_bytes();
                writer.write_all(bytes)?;
                written += bytes.len();
            }
            for s in iter {
                let t1 = self.as_ref().as_bytes();
                writer.write_all(t1)?;
                written += t1.len();

                let t2 = s.as_ref().as_bytes();
                writer.write_all(t2)?;
                written += t2.len();
            }
            writer.flush()?;
            io::Result::Ok(written)
        }
    }

    #[cfg(test)]
    mod tests {
        #[test]
        fn write_join_to_file() {
            let v = vec![];
            let mut cur = Cursor::new(v);
            let written = "=".write_join(&mut cur, &["a", "b", "c"]).expect("Couldn't write joined string");
            let result = cur.into_inner();
            assert_eq!(written, 5);
            assert_eq!(&result[..], "a=b=c".as_bytes());
        }

        #[test]
        fn join_with_heap_string() {
            let equal = String::from("=");
            let result = equal.join(&["a", "b", "c"]);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_with_borrowed_string() {
            let equal = String::from("=");
            let eq = &equal[..];
            let result = eq.join(&["a", "b", "c"]);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_with_cow_string() {
            let equal = String::from("=");
            let eq: Cow<'_, str> = Cow::Borrowed(&equal[..]);
            let result = eq.join(&["a", "b", "c"]);
            assert_eq!("a=b=c", &result[..]);
        }

        #[test]
        fn join_hashset() {
            let set = {
                let mut set = HashSet::new();
                set.insert("a");
                set.insert("b");
                set.insert("c");
                set
            };
            let result = "=".join(&set);
            assert!(vec!["a=b=c", "a=c=b", "b=a=c", "b=c=a", "c=a=b", "c=b=a"].contains(&&result[..]));
        }

        use std::io::Cursor;
        use std::borrow::Cow;
        use std::collections::HashSet;
        use super::Join;
    }

    use std::{
        io::{self, Write},
    };
}