gitbox 2.1.3

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

use crate::{
    application::{
        manager::message_egress_manager::MessageEgressManager,
        manager::{
            bounded_commit_summary_ingress_manager::BoundedCommitSummaryIngressManager,
            version_ingress_manager::VersionIngressManager,
        },
        options::changelog::{ChangelogOptions, FORMAT_PLACEHOLDER},
        repository_impl::{
            bounded_commit_summary_ingress_repository_impl::BoundedCommitSummaryIngressRepositoryImpl,
            semantic_version_ingress_repository_impl::SemanticVersionIngressRepositoryImpl,
        },
    },
    domain::trigger::Trigger,
    usecase::{
        configuration::changelog::{ChangelogConfiguration, ChangelogFormat},
        usecases::{create_changelog::CreateChangelogUseCase, usecase::UseCase},
    },
};

use super::exit_code::ControllerExitCode;

pub struct ChangelogController<'a> {
    options: ChangelogOptions,
    commit_retriever: &'a dyn BoundedCommitSummaryIngressManager,
    version_retriever: &'a dyn VersionIngressManager,
    output_manager: &'a dyn MessageEgressManager,
}

impl<'a, 'b: 'a, 'c: 'a, 'd: 'a> ChangelogController<'a> {
    pub fn new(
        options: ChangelogOptions,
        commit_retriever: &'b dyn BoundedCommitSummaryIngressManager,
        version_retriever: &'c dyn VersionIngressManager,
        output_manager: &'d dyn MessageEgressManager,
    ) -> Self {
        ChangelogController {
            options,
            commit_retriever,
            version_retriever,
            output_manager,
        }
    }

    pub fn changelog(&self) -> ControllerExitCode {
        let trigger: Option<Trigger> = match self.options.exclude_trigger() {
            Some(t) => match Trigger::from_str(t) {
                Ok(v) => Some(v),
                Err(e) => {
                    self.output_manager.error(&e.to_string());
                    return ControllerExitCode::Error(1);
                }
            },
            None => None,
        };
        let configuration = ChangelogConfiguration::new(
            self.options.generate_from_latest_version(),
            ChangelogFormat::new(
                Box::new(|it| {
                    self.options
                        .format()
                        .title()
                        .replace(FORMAT_PLACEHOLDER, it)
                }),
                Box::new(|it| self.options.format().typ().replace(FORMAT_PLACEHOLDER, it)),
                Box::new(|it| {
                    self.options
                        .format()
                        .scope()
                        .replace(FORMAT_PLACEHOLDER, it)
                }),
                Box::new(|it| self.options.format().list().replace(FORMAT_PLACEHOLDER, it)),
                Box::new(|it| self.options.format().item().replace(FORMAT_PLACEHOLDER, it)),
                Box::new(|it| {
                    self.options
                        .format()
                        .breaking()
                        .replace(FORMAT_PLACEHOLDER, it)
                }),
            ),
            trigger,
        );
        let bounded_commit_summary_ingress_repository_impl =
            BoundedCommitSummaryIngressRepositoryImpl::new(self.commit_retriever);
        let semantic_version_ingress_repository_impl =
            SemanticVersionIngressRepositoryImpl::new(self.version_retriever);
        let usecase = CreateChangelogUseCase::new(
            configuration,
            &bounded_commit_summary_ingress_repository_impl,
            &semantic_version_ingress_repository_impl,
        );
        match usecase.execute() {
            Ok(c) => {
                self.output_manager.output(&c);
                ControllerExitCode::Ok
            }
            Err(e) => {
                self.output_manager.error(&e.to_string());
                ControllerExitCode::Error(1)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::{error::Error, fmt::Display, rc::Rc};

    use crate::{
        application::{
            controller::exit_code::ControllerExitCode,
            manager::message_egress_manager::MessageEgressManager,
            manager::{
                bounded_commit_summary_ingress_manager::BoundedCommitSummaryIngressManager,
                version_ingress_manager::VersionIngressManager,
            },
            options::changelog::{ChangelogFormatOptions, ChangelogOptions},
        },
        domain::semantic_version::SemanticVersion,
        usecase::type_aliases::AnyError,
    };

    use super::ChangelogController;

    struct MockCommitRetriever {}
    impl BoundedCommitSummaryIngressManager for MockCommitRetriever {
        fn get_commits_from(
            &self,
            _version: Rc<Option<SemanticVersion>>,
        ) -> Result<Box<dyn DoubleEndedIterator<Item = String>>, AnyError> {
            Ok(Box::new(
                vec![
                    "feat: test".to_owned(),
                    "fix: test".to_owned(),
                    "test: test".to_owned(),
                ]
                .into_iter(),
            ))
        }
    }

    #[derive(Debug)]
    struct MockVersionError {}
    impl Display for MockVersionError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "MockVersionError")
        }
    }

    impl Error for MockVersionError {}

    struct MockVersionRetriever {}
    impl VersionIngressManager for MockVersionRetriever {
        fn last_version(&self) -> Result<Option<String>, AnyError> {
            Err(Box::new(MockVersionError {}))
        }

        fn last_stable_version(&self) -> Result<Option<String>, AnyError> {
            Ok(Some("0.1.0".to_owned()))
        }
    }

    struct MockOutputManager {}
    impl MessageEgressManager for MockOutputManager {
        fn output(&self, _message: &str) {}

        fn error(&self, _error: &str) {}
    }

    #[test]
    fn wrong_trigger_exits_with_error() {
        let options = ChangelogOptions::new(
            false,
            ChangelogFormatOptions::new(
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
            )
            .expect("hand-crafted options are correct"),
            Some("abc".to_string()),
        );
        let commit_retriever = MockCommitRetriever {};
        let version_retriever = MockVersionRetriever {};
        let output_manager = MockOutputManager {};
        let controller = ChangelogController::new(
            options,
            &commit_retriever,
            &version_retriever,
            &output_manager,
        );
        let result = controller.changelog();
        assert!(matches!(result, ControllerExitCode::Error(..)));
    }

    #[test]
    fn correct_usecase_execution() {
        let options = ChangelogOptions::new(
            false,
            ChangelogFormatOptions::new(
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
            )
            .expect("hand-crafted options should be correct"),
            None,
        );
        let commit_retriever = MockCommitRetriever {};
        let version_retriever = MockVersionRetriever {};
        let output_manager = MockOutputManager {};
        let controller = ChangelogController::new(
            options,
            &commit_retriever,
            &version_retriever,
            &output_manager,
        );
        let result = controller.changelog();
        assert!(matches!(result, ControllerExitCode::Ok));
    }

    #[test]
    fn failed_execution_of_usecase() {
        let options = ChangelogOptions::new(
            true,
            ChangelogFormatOptions::new(
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
                String::from("%s"),
            )
            .expect("hand-crafted options should be correct"),
            None,
        );
        let commit_retriever = MockCommitRetriever {};
        let version_retriever = MockVersionRetriever {};
        let output_manager = MockOutputManager {};
        let controller = ChangelogController::new(
            options,
            &commit_retriever,
            &version_retriever,
            &output_manager,
        );
        let result = controller.changelog();
        assert!(matches!(result, ControllerExitCode::Error(..)));
    }
}