nesty 0.1.0

Generate code with with human readable indentation
Documentation
/// # Nesty
///
/// A small crate to help generate human readable code from rust.
///
/// The primary interface is the `code!` macro which looks as follows:
///
/// ```rust
/// # use nesty::{code, Code};
/// # fn main() {
/// code! {
///     [0] "fn main() {";
///     [1]     "println!(\"hello, world\");";
///     [0] "}"
/// };
/// # }
/// ```
///
/// The bracketed numbers give the desired amount of indentation for the line.
///
/// Code blocks can also be nested, like so:
///
/// ```rust
/// # use nesty::{code, Code};
/// let if_expr = code!{
///     [0] "if x > 0 {";
///     [1]     "println(\"found one!\")";
///     [0] "}"
/// };
/// code!{
///     [0] "fn main() {";
///     [1]     if_expr;
///     [0] "}"
/// };
/// # fn main() {}
/// ```
/// which will produce
/// ```notest
/// fn main() {
///     if x > 0 {
///         println!("found one!");
///     }
/// }
/// ```
///
/// The code macro also supports strings, and vectors of strings which will be properly indented,
/// (vectors are assumed to be a vector of lines, strings are indented after each newline)
///
/// With the `diff_assert` feature, the crate also has a `assert_same_code` macro
/// which checks two strings for equality, and if they differ, prints a diff before panicing.

#[cfg(feature = "diff_assert")]
pub mod diff_assert;

pub trait Indentable {
    fn indent(&self, amount: usize) -> Vec<String>;

    fn indent_chars(amount: usize) -> String {
        (0..amount).map(|_| "    ").collect::<Vec<_>>().join("")
    }
}

impl Indentable for String {
    fn indent(&self, amount: usize) -> Vec<String> {
        self.get_lines().indent(amount)
    }
}

impl Indentable for Vec<String> {
    fn indent(&self, amount: usize) -> Vec<String> {
        let indentation = Self::indent_chars(amount);
        self.iter()
            .cloned()
            .map(|l| {
                // If this string is a multi-line string, we should recurse to indent
                // the substring too
                if l.contains("\n") {
                    l.indent(amount)
                } else {
                    vec![format!("{}{}", indentation, l)]
                }
            })
            .flatten()
            .collect()
    }
}

pub trait HasLines {
    fn get_lines(&self) -> Vec<String>;
}
impl HasLines for String {
    fn get_lines(&self) -> Vec<String> {
        self.split("\n").map(String::from).collect()
    }
}
impl HasLines for &str {
    fn get_lines(&self) -> Vec<String> {
        self.split("\n").map(String::from).collect()
    }
}
impl HasLines for Vec<String> {
    fn get_lines(&self) -> Vec<String> {
        self.clone()
    }
}
impl HasLines for Code {
    fn get_lines(&self) -> Vec<String> {
        self.lines.clone()
    }
}
impl HasLines for Vec<Code> {
    fn get_lines(&self) -> Vec<String> {
        let mut result = Code::new();
        for code in self {
            result.join(code)
        }
        result.get_lines()
    }
}

impl<T> HasLines for &T
where
    T: HasLines,
{
    fn get_lines(&self) -> Vec<String> {
        (*self).get_lines()
    }
}

pub struct Code {
    lines: Vec<String>,
}

impl Code {
    pub fn new() -> Self {
        Code { lines: vec![] }
    }

    pub fn to_string(&self) -> String {
        self.lines.join("\n")
    }

    pub fn join(&mut self, other: impl HasLines) {
        self.lines.append(&mut other.get_lines())
    }

    pub fn join_indent(&mut self, amount: usize, other: impl HasLines) {
        self.lines.append(&mut other.get_lines().indent(amount))
    }
}

#[macro_export]
macro_rules! code {
    () => {
        Code::new()
    };
    ($code:ident, [$amount:expr] $lines:expr) => {
        $code.join_indent($amount, $lines);
    };
    ($code:ident, [$amount:expr] $lines:expr$(; $([$r_amount:expr] $r_lines:expr);*)?$(;)?) => {
        code!{$code, [$amount] $lines};
        $(code!{$code, $([$r_amount] $r_lines);*})?
    };
    ($([$r_amount:expr] $r_lines:expr);*$(;)?) =>{
        {
            let mut code = Code::new();
            code!{code, $([$r_amount] $r_lines);*};
            code
        }
    }
}

#[cfg(feature = "diff_assert")]
#[cfg(test)]
mod tests {
    use super::*;

    use crate as nesty;
    use indoc::indoc;

    #[test]
    fn basic_test() {
        let result = code!(
            [0] "fn main() {";
            [1]     &"println!(\"hello, world\")";
            [0] "}"
        );

        let expected = indoc!(
            r#"
            fn main() {
                println!("hello, world")
            }"#
        );

        assert_same_code!(&result.to_string(), &expected)
    }

    #[test]
    fn trailing_semis_are_allowed() {
        let result = code!(
            [0] "fn main() {";
            [1]     &"println!(\"hello, world\")";
            [0] "}";
        );

        let expected = indoc!(
            r#"
            fn main() {
                println!("hello, world")
            }"#
        );

        assert_same_code!(&result.to_string(), &expected)
    }
}

#[cfg(not(feature = "diff_assert"))]
#[cfg(test)]
mod test {
    #[test]
    fn dummy_test() {
        panic!("Tests require the diff_assert feature to run")
    }
}