gitbox 2.1.3

Git toolbox to simplify adoption of conventional commits and semantic version, among other things.
Documentation
use regex::Regex;
use requestty::{prompt_one, Answer, Question};

use crate::{
    domain::conventional_commit_summary::ConventionalCommitSummary, usecase::type_aliases::AnyError,
};

use super::{
    gitextra_egress_helper::GitExtraEgressHelper, gitextra_ingress_helper::GitExtraIngressHelper,
};

pub struct CommitPromptHelper<'a> {
    gitextra_read_manager: &'a dyn GitExtraIngressHelper,
    gitextra_append_manager: &'a dyn GitExtraEgressHelper,
}

impl<'a, 'b: 'a, 'c: 'a> CommitPromptHelper<'a> {
    pub fn new(
        gitextra_read_manager: &'b dyn GitExtraIngressHelper,
        gitextra_append_manager: &'c dyn GitExtraEgressHelper,
    ) -> Self {
        CommitPromptHelper {
            gitextra_read_manager,
            gitextra_append_manager,
        }
    }

    pub fn ask_type(&self) -> Result<String, AnyError> {
        let available_types = self.gitextra_read_manager.get_types()?;
        let raw_select = Question::raw_select("commit_type")
            .message("Choose the commit type:")
            .choices(&available_types)
            .choice("Create new type")
            .build();
        let answer = prompt_one(raw_select);
        let answer_index = match answer {
            Ok(Answer::ListItem(a)) => a.index,
            Ok(_) => panic!("Obtained a non ListItem from a raw_select"),
            Err(e) => return Err(Box::new(e)),
        };
        if answer_index == available_types.len() {
            self.ask_new_type()
        } else {
            Ok(available_types[answer_index].clone())
        }
    }

    pub fn ask_scope(&self) -> Result<Option<String>, AnyError> {
        let available_scopes = self.gitextra_read_manager.get_scopes()?;
        let raw_select = Question::raw_select("scope")
            .message("Choose the scope:")
            .choice("None")
            .choices(&available_scopes)
            .choice("Create new scope")
            .build();
        let answer = prompt_one(raw_select);
        let answer_index = match answer {
            Ok(Answer::ListItem(a)) => a.index,
            Ok(_) => panic!("Obtained a non ListItem from a raw_select"),
            Err(e) => return Err(Box::new(e)),
        };
        Ok(if answer_index == available_scopes.len() + 1 {
            Some(self.ask_new_scope()?)
        } else if answer_index == 0 {
            None
        } else {
            Some(available_scopes[answer_index - 1].clone())
        })
    }

    pub fn ask_breaking(&self) -> Result<bool, AnyError> {
        let answer = prompt_one(
            Question::confirm("breaking")
                .message("Is this commit a breaking change?")
                .build(),
        );
        match answer {
            Ok(Answer::Bool(breaking)) => Ok(breaking),
            Ok(_) => panic!("Obtained a non Bool from a confirm"),
            Err(e) => Err(Box::new(e)),
        }
    }

    pub fn ask_summary(&self) -> Result<String, AnyError> {
        let answer = prompt_one(
            Question::input("summary")
                .message("Commit summary:")
                .validate(|s, _| {
                    if !s.is_empty() {
                        Ok(())
                    } else {
                        Err("The summary cannot be empty".to_owned())
                    }
                })
                .build(),
        );
        match answer {
            Ok(Answer::String(s)) => Ok(s),
            Ok(_) => panic!("Obtained a non String from an input"),
            Err(e) => Err(Box::new(e)),
        }
    }

    pub fn ask_body(&self) -> Result<Option<String>, AnyError> {
        let answer = prompt_one(
            Question::editor("body")
                .message("Insert the body of the commit message")
                .extension(".txt")
                .build(),
        );
        match answer {
            Ok(Answer::String(s)) => Ok(if s.is_empty() { None } else { Some(s) }),
            Ok(_) => panic!("Obtained a non String from an input"),
            Err(e) => Err(Box::new(e)),
        }
    }

    fn ask_new_type(&self) -> Result<String, AnyError> {
        let new_type = self.ask_new_input("type", ConventionalCommitSummary::type_pattern())?;
        self.gitextra_append_manager.append_type(&new_type)?;
        Ok(new_type)
    }

    fn ask_new_scope(&self) -> Result<String, AnyError> {
        let new_scope = self.ask_new_input("scope", ConventionalCommitSummary::scope_pattern())?;
        self.gitextra_append_manager.append_scope(&new_scope)?;
        Ok(new_scope)
    }

    fn ask_new_input(&self, what: &str, valid_regex: &Regex) -> Result<String, AnyError> {
        let new_value = prompt_one(
            Question::input(format!("new-{}", what))
                .message(format!("New {}: ", what))
                .validate(|s, _| {
                    if valid_regex.is_match(s) {
                        Ok(())
                    } else {
                        Err(format!("{} not valid", what))
                    }
                })
                .build(),
        );
        match new_value {
            Ok(Answer::String(s)) => Ok(s),
            Ok(_) => panic!("Obtained a non String from an input"),
            Err(e) => Err(Box::new(e)),
        }
    }
}