gitbox 2.1.3

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

use regex::Regex;

use crate::{
    application::{
        manager::{
            bounded_commit_summary_ingress_manager::BoundedCommitSummaryIngressManager,
            commit_metadata_ingress_manager::CommitMetadataIngressManager,
            version_ingress_manager::VersionIngressManager,
        },
        manager::{
            message_egress_manager::MessageEgressManager, tag_egress_manager::TagEgressManager,
        },
        options::describe::{DescribeOptions, PRERELEASE_NUM_PLACEHOLDER},
        repository_impl::{
            bounded_commit_summary_ingress_repository_impl::BoundedCommitSummaryIngressRepositoryImpl,
            commit_metadata_ingress_repository_impl::CommitMetadataIngressRepositoryImpl,
            semantic_version_ingress_repository_impl::SemanticVersionIngressRepositoryImpl,
            tag_egress_repository_impl::TagEgressRepositoryImpl,
        },
    },
    domain::trigger::Trigger,
    usecase::{
        configuration::{
            describe::{
                DescribeConfiguration, DescribeMetadataConfiguration,
                DescribePrereleaseConfiguration, DescribeTriggerConfiguration,
            },
            tag::TagConfiguration,
        },
        type_aliases::AnyError,
        usecases::{
            create_tag::CreateTagUseCase, describe_new_version::CalculateNewVersionUseCase,
            usecase::UseCase,
        },
    },
};

use super::exit_code::ControllerExitCode;

pub struct DescribeController<'a> {
    options: DescribeOptions,
    commit_summary_manager: &'a dyn BoundedCommitSummaryIngressManager,
    commit_metadata_manager: &'a dyn CommitMetadataIngressManager,
    version_manager: &'a dyn VersionIngressManager,
    tag_write_manager: &'a dyn TagEgressManager,
    output_manager: &'a dyn MessageEgressManager,
}

impl<'a, 'b: 'a, 'c: 'a, 'd: 'a, 'e: 'a, 'f: 'a> DescribeController<'a> {
    pub fn new(
        options: DescribeOptions,
        commit_summary_manager: &'b dyn BoundedCommitSummaryIngressManager,
        commit_metadata_manager: &'c dyn CommitMetadataIngressManager,
        version_manager: &'d dyn VersionIngressManager,
        tag_write_manager: &'e dyn TagEgressManager,
        output_manager: &'f dyn MessageEgressManager,
    ) -> Self {
        DescribeController {
            options,
            commit_summary_manager,
            commit_metadata_manager,
            version_manager,
            tag_write_manager,
            output_manager,
        }
    }

    pub fn describe(&self) -> ControllerExitCode {
        match self.run() {
            Ok(_) => ControllerExitCode::Ok,
            Err(e) => {
                self.output_manager.error(&e.to_string());
                ControllerExitCode::Error(1)
            }
        }
    }

    fn run(&self) -> Result<(), AnyError> {
        let describe_configuration = self.generate_describe_configuration()?;
        let commit_summary_repository =
            BoundedCommitSummaryIngressRepositoryImpl::new(self.commit_summary_manager);
        let commit_metadata_repository =
            CommitMetadataIngressRepositoryImpl::new(self.commit_metadata_manager);
        let version_repository = SemanticVersionIngressRepositoryImpl::new(self.version_manager);
        let describe_usecase = CalculateNewVersionUseCase::new(
            describe_configuration,
            &commit_summary_repository,
            &commit_metadata_repository,
            &version_repository,
        );
        let (new_version, old_version) = describe_usecase.execute()?;
        if self.options.diff() {
            self.output_manager.output(&format!(
                "Previous version: {}",
                old_version.map_or_else(|| String::from("None"), |it| it.to_string())
            ));
        }
        self.output_manager.output(&new_version.to_string());
        if self.options.tag().enabled() {
            let tag_configuration = TagConfiguration::new(
                new_version,
                self.options.tag().message().map(|it| it.to_owned()),
                self.options.tag().sign_enabled(),
            )?;
            let tag_write_repository = TagEgressRepositoryImpl::new(self.tag_write_manager);
            let tag_usecase = CreateTagUseCase::new(tag_configuration, &tag_write_repository);
            tag_usecase.execute()?;
            self.output_manager.output("Tag created successfully");
        }
        Ok(())
    }

    const DEFAULT_MAJOR_TRIGGER_STR: &'static str = "breaking";
    const DEFAULT_MINOR_TRIGGER_STR: &'static str = "type IN [ feat ]";
    const DEFAULT_PATCH_TRIGGER_STR: &'static str = "type IN [ fix ]";

    fn generate_describe_configuration(&self) -> Result<DescribeConfiguration, AnyError> {
        let prerelease_configuration = DescribePrereleaseConfiguration::new(
            self.options.prerelease().enabled(),
            Box::new(|it| {
                self.options
                    .prerelease()
                    .pattern()
                    .replace(PRERELEASE_NUM_PLACEHOLDER, &it.to_string())
            }),
            Box::new(|it| {
                let regex = Regex::new(
                    &self
                        .options
                        .prerelease()
                        .old_pattern()
                        .replace(PRERELEASE_NUM_PLACEHOLDER, "(\\d+)"),
                )
                .unwrap();
                regex
                    .captures(it)
                    .expect("regex should match")
                    .get(1)
                    .expect("group 1 must be present to match")
                    .as_str()
                    .parse()
                    .unwrap()
            }),
            self.options.prerelease().pattern() != self.options.prerelease().old_pattern(),
        );
        let metadata_configuration =
            DescribeMetadataConfiguration::new(self.options.metadata().specs().to_vec());
        let trigger_configuration = DescribeTriggerConfiguration::new(
            Trigger::from_str(
                self.options
                    .triggers()
                    .major()
                    .unwrap_or(Self::DEFAULT_MAJOR_TRIGGER_STR),
            )?,
            Trigger::from_str(
                self.options
                    .triggers()
                    .minor()
                    .unwrap_or(Self::DEFAULT_MINOR_TRIGGER_STR),
            )?,
            Trigger::from_str(
                self.options
                    .triggers()
                    .patch()
                    .unwrap_or(Self::DEFAULT_PATCH_TRIGGER_STR),
            )?,
        );
        Ok(DescribeConfiguration::new(
            prerelease_configuration,
            metadata_configuration,
            trigger_configuration,
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::{cell::RefCell, rc::Rc};

    use crate::{
        application::{
            controller::{describe::DescribeController, exit_code::ControllerExitCode},
            manager::{
                bounded_commit_summary_ingress_manager::BoundedCommitSummaryIngressManager,
                commit_metadata_ingress_manager::CommitMetadataIngressManager,
                message_egress_manager::MessageEgressManager, tag_egress_manager::TagEgressManager,
                version_ingress_manager::VersionIngressManager,
            },
            options::describe::{
                DescribeMetadataOptions, DescribeOptions, DescribePrereleaseOptions,
                DescribeTagOptions, DescribeTriggerOptions,
            },
        },
        domain::semantic_version::SemanticVersion,
        usecase::{metadata_spec::MetadataSpec, type_aliases::AnyError},
    };

    struct MockCommitSummaryManager {}
    impl BoundedCommitSummaryIngressManager for MockCommitSummaryManager {
        fn get_commits_from(
            &self,
            _version: Rc<Option<SemanticVersion>>,
        ) -> Result<Box<dyn DoubleEndedIterator<Item = String>>, AnyError> {
            Ok(Box::new(
                vec![
                    "feat: add a feature".to_string(),
                    "test(api): add test for API".to_string(),
                    "refactor: refactor struct".to_string(),
                ]
                .into_iter(),
            ))
        }
    }

    struct MockCommitMetadataManager {}
    impl CommitMetadataIngressManager for MockCommitMetadataManager {
        fn get_metadata(&self, metadata_spec: &MetadataSpec) -> Result<String, AnyError> {
            Ok(match metadata_spec {
                MetadataSpec::Sha => "sha",
                MetadataSpec::Date => "date",
            }
            .to_string())
        }
    }

    struct MockSemanticVersionIngressManager {}
    impl VersionIngressManager for MockSemanticVersionIngressManager {
        fn last_version(&self) -> Result<Option<String>, AnyError> {
            Ok(None)
        }
        fn last_stable_version(&self) -> Result<Option<String>, AnyError> {
            Ok(None)
        }
    }

    struct MockTagEgressManager {
        label: RefCell<Box<str>>,
    }
    impl MockTagEgressManager {
        pub fn new() -> Self {
            MockTagEgressManager {
                label: RefCell::new("".into()),
            }
        }
    }
    impl TagEgressManager for MockTagEgressManager {
        fn create_tag(
            &self,
            label: &str,
            _message: Option<&str>,
            _sign: bool,
        ) -> Result<(), AnyError> {
            self.label.replace(label.into());
            Ok(())
        }
    }

    struct MockOutputManager {
        output_buffer: RefCell<Vec<String>>,
        error_buffer: RefCell<Vec<String>>,
    }
    impl MockOutputManager {
        pub fn new() -> Self {
            MockOutputManager {
                output_buffer: RefCell::new(vec![]),
                error_buffer: RefCell::new(vec![]),
            }
        }
    }
    impl MessageEgressManager for MockOutputManager {
        fn output(&self, message: &str) {
            self.output_buffer.borrow_mut().push(message.to_string());
        }
        fn error(&self, error: &str) {
            self.error_buffer.borrow_mut().push(error.to_string());
        }
    }

    #[test]
    fn basic_usage() {
        let options = DescribeOptions::new(
            DescribePrereleaseOptions::new(false, "dev%d".to_string(), "dev%d".to_string())
                .expect("hand-crafted options are correct"),
            false,
            DescribeMetadataOptions::new(vec![]),
            DescribeTriggerOptions::new(None, None, None),
            DescribeTagOptions::new(false, None, false),
        );
        let commit_summary_manager = MockCommitSummaryManager {};
        let commit_metadata_ingress_manager = MockCommitMetadataManager {};
        let version_ingress_manager = MockSemanticVersionIngressManager {};
        let tag_egress_manager = MockTagEgressManager::new();
        let output_manager = MockOutputManager::new();
        let controller = DescribeController::new(
            options,
            &commit_summary_manager,
            &commit_metadata_ingress_manager,
            &version_ingress_manager,
            &tag_egress_manager,
            &output_manager,
        );
        let result = controller.describe();
        assert!(matches!(result, ControllerExitCode::Ok));
        assert_eq!(output_manager.output_buffer.borrow().as_ref(), ["0.1.0"]);
    }

    #[test]
    fn diff_enabled() {
        let options = DescribeOptions::new(
            DescribePrereleaseOptions::new(false, "dev%d".to_string(), "dev%d".to_string())
                .expect("hand-crafted options are correct"),
            true,
            DescribeMetadataOptions::new(vec![]),
            DescribeTriggerOptions::new(None, None, None),
            DescribeTagOptions::new(false, None, false),
        );
        let commit_summary_manager = MockCommitSummaryManager {};
        let commit_metadata_ingress_manager = MockCommitMetadataManager {};
        let version_ingress_manager = MockSemanticVersionIngressManager {};
        let tag_egress_manager = MockTagEgressManager::new();
        let output_manager = MockOutputManager::new();
        let controller = DescribeController::new(
            options,
            &commit_summary_manager,
            &commit_metadata_ingress_manager,
            &version_ingress_manager,
            &tag_egress_manager,
            &output_manager,
        );
        let result = controller.describe();
        assert!(matches!(result, ControllerExitCode::Ok));
        assert_eq!(
            output_manager.output_buffer.borrow().as_ref(),
            ["Previous version: None", "0.1.0"]
        );
    }

    #[test]
    fn tag_enabled() {
        let options = DescribeOptions::new(
            DescribePrereleaseOptions::new(false, "dev%d".to_string(), "dev%d".to_string())
                .expect("hand-crafted options are correct"),
            false,
            DescribeMetadataOptions::new(vec![]),
            DescribeTriggerOptions::new(None, None, None),
            DescribeTagOptions::new(true, None, false),
        );
        let commit_summary_manager = MockCommitSummaryManager {};
        let commit_metadata_ingress_manager = MockCommitMetadataManager {};
        let version_ingress_manager = MockSemanticVersionIngressManager {};
        let tag_egress_manager = MockTagEgressManager::new();
        let output_manager = MockOutputManager::new();
        let controller = DescribeController::new(
            options,
            &commit_summary_manager,
            &commit_metadata_ingress_manager,
            &version_ingress_manager,
            &tag_egress_manager,
            &output_manager,
        );
        let result = controller.describe();
        assert!(matches!(result, ControllerExitCode::Ok));
        assert_eq!(tag_egress_manager.label.borrow().as_ref(), "0.1.0");
    }
}