gitbox 2.1.3

Git toolbox to simplify adoption of conventional commits and semantic version, among other things.
Documentation
use colored::Colorize;

use crate::{
    domain::tree_graph_line::{TreeGraphLine, TreeGraphLineContent},
    usecase::{
        error::format_tree_error::{FormatTreeError, NoCommitsError},
        repository::treegraphline_ingress_repository::TreeGraphLineIngressRepository,
    },
};

use super::usecase::UseCase;

const TIME_MINIMUM_PADDING: usize = 2;

pub struct FormatTreeGraphUseCase<'a> {
    treegraphline_ingress_repository: &'a dyn TreeGraphLineIngressRepository,
}

impl<'a, 'b: 'a> FormatTreeGraphUseCase<'a> {
    pub fn new(treegraphline_ingress_repository: &'b dyn TreeGraphLineIngressRepository) -> Self {
        FormatTreeGraphUseCase {
            treegraphline_ingress_repository,
        }
    }

    #[inline(always)]
    fn format_line(&self, line: &TreeGraphLine, left_padding: usize) -> Box<str> {
        match line.line_content() {
            TreeGraphLineContent::Metadata(metadata) => format!(
                "{date:>width$} {tree_marks} {hash} {references}",
                date = metadata.relative_date().dimmed(),
                width = left_padding,
                tree_marks = line.tree_marks(),
                hash = metadata.abbreviated_hash().blue(),
                references = metadata.references().yellow(),
            ),
            TreeGraphLineContent::Data(data) => format!(
                "{:>width$} {tree_marks:>1}     {author} {summary}",
                "",
                width = left_padding,
                tree_marks = line.tree_marks(),
                author = data.author().white().bold(),
                summary = data.summary()
            ),
        }
        .into()
    }
}

impl UseCase<Box<str>, FormatTreeError> for FormatTreeGraphUseCase<'_> {
    fn execute(&self) -> Result<Box<str>, FormatTreeError> {
        let lines = self.treegraphline_ingress_repository.graph_lines()?;
        if lines.is_empty() {
            return Err(NoCommitsError::new().into());
        }
        let time_padding = lines
            .iter()
            .filter_map(|it| {
                match it.line_content() {
                    TreeGraphLineContent::Data(_) => None,
                    TreeGraphLineContent::Metadata(metadata) => Some(metadata.relative_date()),
                }
                .map(|it| it.len())
            })
            .max()
            .unwrap_or(0usize);
        let result = lines
            .iter()
            .map(|line| {
                let left_padding = TIME_MINIMUM_PADDING + time_padding;
                self.format_line(line, left_padding)
            })
            .fold(String::new(), |acc, e| acc + "\n" + &e);
        Ok(result.trim_start_matches('\n').into())
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        domain::tree_graph_line::{
            CommitData, CommitMetadata, TreeGraphLine, TreeGraphLineContent,
        },
        usecase::{
            error::format_tree_error::FormatTreeError,
            repository::treegraphline_ingress_repository::TreeGraphLineIngressRepository,
            type_aliases::AnyError,
            usecases::{format_tree_graph::FormatTreeGraphUseCase, usecase::UseCase},
        },
    };

    struct MockTreeGraphLineIngressRepository {}

    impl TreeGraphLineIngressRepository for MockTreeGraphLineIngressRepository {
        fn graph_lines(&self) -> Result<Box<[TreeGraphLine]>, AnyError> {
            Ok([
                TreeGraphLine::new(
                    "*",
                    TreeGraphLineContent::Metadata(
                        CommitMetadata::new("abcdef0", "( sample date 1 )", "( HEAD -> main )")
                            .expect("Hand-crafted lines are always correct"),
                    ),
                ),
                TreeGraphLine::new(
                    "| ",
                    TreeGraphLineContent::Data(
                        CommitData::new("asperan:", "test message")
                            .expect("Hand-crafted lines are always correct"),
                    ),
                ),
                TreeGraphLine::new(
                    "*",
                    TreeGraphLineContent::Metadata(
                        CommitMetadata::new("0fedcba", "( sample date 2 )", "")
                            .expect("Hand-crafted lines are always correct"),
                    ),
                ),
                TreeGraphLine::new(
                    "| ",
                    TreeGraphLineContent::Data(
                        CommitData::new("asperan:", "another test message")
                            .expect("Hand-crafted lines are always correct"),
                    ),
                ),
            ]
            .into())
        }
    }

    struct EmptyTreeGraphLineIngressRepsitory {}

    impl TreeGraphLineIngressRepository for EmptyTreeGraphLineIngressRepsitory {
        fn graph_lines(&self) -> Result<Box<[TreeGraphLine]>, AnyError> {
            Ok([].into())
        }
    }

    #[test]
    fn format_header_line() {
        let padding = 16;
        let t = TreeGraphLine::new(
            "*",
            TreeGraphLineContent::Metadata(
                CommitMetadata::new("abcdef0", "( sample date )", "( HEAD -> main )")
                    .expect("Hand-crafted lines are always correct"),
            ),
        );
        let usecase = FormatTreeGraphUseCase::new(&MockTreeGraphLineIngressRepository {});
        let result = usecase.format_line(&t, padding);
        let expected = "\u{1b}[2m ( sample date )\u{1b}[0m * \u{1b}[34mabcdef0\u{1b}[0m \u{1b}[33m( HEAD -> main )\u{1b}[0m";
        assert_eq!(result, expected.into());
    }

    #[test]
    fn format_message_line() {
        let padding = 16;
        let t = TreeGraphLine::new(
            "| ",
            TreeGraphLineContent::Data(
                CommitData::new("asperan:", "test message")
                    .expect("Hand-crafted lines are always correct"),
            ),
        );
        let usecase = FormatTreeGraphUseCase::new(&MockTreeGraphLineIngressRepository {});
        let result = usecase.format_line(&t, padding);
        let expected = "                 |     \u{1b}[1;37masperan:\u{1b}[0m test message";
        assert_eq!(result, expected.into());
    }

    #[test]
    fn execute_complete() {
        let usecase = FormatTreeGraphUseCase::new(&MockTreeGraphLineIngressRepository {});
        let result = usecase
            .execute()
            .expect("The usecase should execute correctly");
        println!("{}", &result);
        let expected = concat!(
            "\u{1b}[2m  ( sample date 1 )\u{1b}[0m * \u{1b}[34mabcdef0\u{1b}[0m \u{1b}[33m( HEAD -> main )\u{1b}[0m\n",
            "                    |     \u{1b}[1;37masperan:\u{1b}[0m test message\n",
            "\u{1b}[2m  ( sample date 2 )\u{1b}[0m * \u{1b}[34m0fedcba\u{1b}[0m \u{1b}[33m\u{1b}[0m\n",
            "                    |     \u{1b}[1;37masperan:\u{1b}[0m another test message",
        );
        assert_eq!(result, expected.into());
    }

    #[test]
    fn execute_empty() {
        let usecase = FormatTreeGraphUseCase::new(&EmptyTreeGraphLineIngressRepsitory {});
        let result = usecase.execute();
        assert!(matches!(result, Err(FormatTreeError::NoCommits(_))));
    }
}