actrpc-cli 0.1.0

Command-line tools for ActRPC.
use actrpc_orchestrator::{
    action::{
        ActionHandlerFuture,
        actions::request_review::{
            REVIEW_DECISION_APPROVED, REVIEW_DECISION_DENIED, RequestReviewParams,
            RequestReviewResult,
        },
    },
    error::ActionExecutionError,
    review::ReviewProvider,
};
use std::io::{self, Write};

#[derive(Debug, Default)]
pub struct CliReviewProvider;

impl ReviewProvider for CliReviewProvider {
    fn request_review<'a>(
        &'a self,
        params: RequestReviewParams,
    ) -> ActionHandlerFuture<'a, Result<RequestReviewResult, ActionExecutionError>> {
        Box::pin(async move {
            tokio::task::spawn_blocking(move || request_review_blocking(params))
                .await
                .map_err(|source| ActionExecutionError::InvalidState {
                    message: format!("review prompt task failed: {source}"),
                })?
        })
    }
}

fn request_review_blocking(
    params: RequestReviewParams,
) -> Result<RequestReviewResult, ActionExecutionError> {
    eprintln!();
    eprintln!("Review required");
    eprintln!("Rule: {}", params.rule_name);
    eprintln!("Title: {}", params.title);
    eprintln!("Severity: {}", params.severity);
    eprintln!("Reason: {}", params.reason);
    eprintln!();

    loop {
        eprint!("Approve? [y/N]: ");

        io::stderr()
            .flush()
            .map_err(|source| ActionExecutionError::InvalidState {
                message: format!("failed to flush review prompt: {source}"),
            })?;

        let mut input = String::new();

        io::stdin()
            .read_line(&mut input)
            .map_err(|source| ActionExecutionError::InvalidState {
                message: format!("failed to read review response: {source}"),
            })?;

        match parse_review_decision_input(&input) {
            Some(decision) => return Ok(RequestReviewResult { decision }),
            None => eprintln!("Please answer y or n."),
        }
    }
}

fn parse_review_decision_input(input: &str) -> Option<String> {
    match input.trim().to_ascii_lowercase().as_str() {
        "y" | "yes" => Some(REVIEW_DECISION_APPROVED.to_owned()),
        "" | "n" | "no" => Some(REVIEW_DECISION_DENIED.to_owned()),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use actrpc_orchestrator::action::actions::request_review::{
        REVIEW_DECISION_APPROVED, REVIEW_DECISION_DENIED,
    };

    #[test]
    fn parse_review_decision_accepts_yes_variants() {
        assert_eq!(
            parse_review_decision_input("y"),
            Some(REVIEW_DECISION_APPROVED.to_owned())
        );
        assert_eq!(
            parse_review_decision_input("yes"),
            Some(REVIEW_DECISION_APPROVED.to_owned())
        );
        assert_eq!(
            parse_review_decision_input(" YES "),
            Some(REVIEW_DECISION_APPROVED.to_owned())
        );
    }

    #[test]
    fn parse_review_decision_accepts_no_variants_and_empty_default() {
        assert_eq!(
            parse_review_decision_input("n"),
            Some(REVIEW_DECISION_DENIED.to_owned())
        );
        assert_eq!(
            parse_review_decision_input("no"),
            Some(REVIEW_DECISION_DENIED.to_owned())
        );
        assert_eq!(
            parse_review_decision_input(""),
            Some(REVIEW_DECISION_DENIED.to_owned())
        );
        assert_eq!(
            parse_review_decision_input("   "),
            Some(REVIEW_DECISION_DENIED.to_owned())
        );
    }

    #[test]
    fn parse_review_decision_rejects_unknown_input() {
        assert_eq!(parse_review_decision_input("maybe"), None);
        assert_eq!(parse_review_decision_input("approve"), None);
        assert_eq!(parse_review_decision_input("deny"), None);
    }
}