cargo-changeset 0.1.7

A cargo subcommand for managing changesets
pub(crate) trait CliWriter {
    fn blank(&self);
    fn message(&self, level: MessageLevel, text: &str);
    fn heading(&self, text: &str);
    fn line(&self, text: &str);
    fn indented(&self, text: &str);
    fn detail(&self, key: &str, value: &str);
    fn list_item(&self, text: &str);
    fn warn_stderr(&self, text: &str);
    fn raw(&self, text: &str);
    fn raw_stderr(&self, text: &str);
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MessageLevel {
    Info,
    Success,
    Hint,
}

pub(crate) struct StdoutCliWriter;

impl CliWriter for StdoutCliWriter {
    fn blank(&self) {
        println!();
    }

    fn message(&self, _level: MessageLevel, text: &str) {
        println!("{text}");
    }

    fn heading(&self, text: &str) {
        println!("{text}");
    }

    fn line(&self, text: &str) {
        println!("{text}");
    }

    fn indented(&self, text: &str) {
        println!("  {text}");
    }

    fn detail(&self, key: &str, value: &str) {
        println!("  {key} = {value}");
    }

    fn list_item(&self, text: &str) {
        println!("  - {text}");
    }

    fn warn_stderr(&self, text: &str) {
        eprintln!("{text}");
    }

    fn raw(&self, text: &str) {
        print!("{text}");
    }

    fn raw_stderr(&self, text: &str) {
        eprint!("{text}");
    }
}

#[cfg(test)]
pub(crate) mod test_support {
    use std::cell::RefCell;

    use super::{CliWriter, MessageLevel};

    #[derive(Debug, Clone, PartialEq, Eq)]
    pub(crate) enum OutputEntry {
        Blank,
        Message { level: MessageLevel, text: String },
        Heading(String),
        Line(String),
        Indented(String),
        Detail { key: String, value: String },
        ListItem(String),
        Raw(String),
    }

    pub(crate) struct BufferCliWriter {
        stdout: RefCell<Vec<OutputEntry>>,
        stderr: RefCell<Vec<String>>,
    }

    impl BufferCliWriter {
        pub(crate) fn new() -> Self {
            Self {
                stdout: RefCell::new(Vec::new()),
                stderr: RefCell::new(Vec::new()),
            }
        }

        pub(crate) fn stdout_entries(&self) -> Vec<OutputEntry> {
            self.stdout.borrow().clone()
        }

        pub(crate) fn stderr_entries(&self) -> Vec<String> {
            self.stderr.borrow().clone()
        }

        pub(crate) fn stdout_text(&self) -> String {
            let entries = self.stdout.borrow();
            let mut output = String::new();
            for entry in entries.iter() {
                match entry {
                    OutputEntry::Blank => output.push('\n'),
                    OutputEntry::Message { text, .. } => {
                        output.push_str(text);
                        output.push('\n');
                    }
                    OutputEntry::Heading(text) | OutputEntry::Line(text) => {
                        output.push_str(text);
                        output.push('\n');
                    }
                    OutputEntry::Indented(text) => {
                        output.push_str("  ");
                        output.push_str(text);
                        output.push('\n');
                    }
                    OutputEntry::Detail { key, value } => {
                        output.push_str("  ");
                        output.push_str(key);
                        output.push_str(" = ");
                        output.push_str(value);
                        output.push('\n');
                    }
                    OutputEntry::ListItem(text) => {
                        output.push_str("  - ");
                        output.push_str(text);
                        output.push('\n');
                    }
                    OutputEntry::Raw(text) => {
                        output.push_str(text);
                    }
                }
            }
            output
        }
    }

    impl CliWriter for BufferCliWriter {
        fn blank(&self) {
            self.stdout.borrow_mut().push(OutputEntry::Blank);
        }

        fn message(&self, level: MessageLevel, text: &str) {
            self.stdout.borrow_mut().push(OutputEntry::Message {
                level,
                text: text.to_string(),
            });
        }

        fn heading(&self, text: &str) {
            self.stdout
                .borrow_mut()
                .push(OutputEntry::Heading(text.to_string()));
        }

        fn line(&self, text: &str) {
            self.stdout
                .borrow_mut()
                .push(OutputEntry::Line(text.to_string()));
        }

        fn indented(&self, text: &str) {
            self.stdout
                .borrow_mut()
                .push(OutputEntry::Indented(text.to_string()));
        }

        fn detail(&self, key: &str, value: &str) {
            self.stdout.borrow_mut().push(OutputEntry::Detail {
                key: key.to_string(),
                value: value.to_string(),
            });
        }

        fn list_item(&self, text: &str) {
            self.stdout
                .borrow_mut()
                .push(OutputEntry::ListItem(text.to_string()));
        }

        fn warn_stderr(&self, text: &str) {
            self.stderr.borrow_mut().push(text.to_string());
        }

        fn raw(&self, text: &str) {
            self.stdout
                .borrow_mut()
                .push(OutputEntry::Raw(text.to_string()));
        }

        fn raw_stderr(&self, text: &str) {
            self.stderr.borrow_mut().push(text.to_string());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::test_support::{BufferCliWriter, OutputEntry};
    use super::{CliWriter, MessageLevel};

    #[test]
    fn blank_records_blank_entry() {
        let writer = BufferCliWriter::new();
        writer.blank();

        assert_eq!(writer.stdout_entries(), vec![OutputEntry::Blank]);
    }

    #[test]
    fn message_records_level_and_text() {
        let writer = BufferCliWriter::new();
        writer.message(MessageLevel::Success, "done");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Message {
                level: MessageLevel::Success,
                text: "done".to_string()
            }]
        );
    }

    #[test]
    fn heading_records_text() {
        let writer = BufferCliWriter::new();
        writer.heading("Title");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Heading("Title".to_string())]
        );
    }

    #[test]
    fn line_records_text() {
        let writer = BufferCliWriter::new();
        writer.line("hello");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Line("hello".to_string())]
        );
    }

    #[test]
    fn indented_records_text() {
        let writer = BufferCliWriter::new();
        writer.indented("item");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Indented("item".to_string())]
        );
    }

    #[test]
    fn detail_records_key_value() {
        let writer = BufferCliWriter::new();
        writer.detail("commit", "true");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Detail {
                key: "commit".to_string(),
                value: "true".to_string()
            }]
        );
    }

    #[test]
    fn list_item_records_text() {
        let writer = BufferCliWriter::new();
        writer.list_item("first");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::ListItem("first".to_string())]
        );
    }

    #[test]
    fn warn_stderr_records_to_stderr_buffer() {
        let writer = BufferCliWriter::new();
        writer.warn_stderr("warning!");

        assert!(writer.stdout_entries().is_empty());
        assert_eq!(writer.stderr_entries(), vec!["warning!".to_string()]);
    }

    #[test]
    fn raw_stderr_records_to_stderr_buffer() {
        let writer = BufferCliWriter::new();
        writer.raw_stderr("partial error");

        assert!(writer.stdout_entries().is_empty());
        assert_eq!(writer.stderr_entries(), vec!["partial error".to_string()]);
    }

    #[test]
    fn raw_records_text_without_newline() {
        let writer = BufferCliWriter::new();
        writer.raw("partial");

        assert_eq!(
            writer.stdout_entries(),
            vec![OutputEntry::Raw("partial".to_string())]
        );
    }

    #[test]
    fn stdout_text_renders_all_entry_types() {
        let writer = BufferCliWriter::new();
        writer.heading("Header");
        writer.blank();
        writer.line("a line");
        writer.indented("indented");
        writer.detail("key", "val");
        writer.list_item("item");
        writer.message(MessageLevel::Info, "info msg");
        writer.raw("raw");

        let text = writer.stdout_text();
        assert_eq!(
            text,
            "Header\n\na line\n  indented\n  key = val\n  - item\ninfo msg\nraw"
        );
    }
}