gha-expression-proof 1.0.0

GitHub Actions expression evaluator and receipt generator for offline CI compatibility testing
Documentation
use crate::model::{CheckStatus, EvaluationReceipt};
use anyhow::Result;
use clap::ValueEnum;

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
    Text,
    Json,
    Markdown,
}

pub fn render_receipt(receipt: &EvaluationReceipt, format: OutputFormat) -> Result<String> {
    match format {
        OutputFormat::Text => Ok(render_text(receipt)),
        OutputFormat::Json => Ok(format!("{}\n", serde_json::to_string_pretty(receipt)?)),
        OutputFormat::Markdown => Ok(render_markdown(receipt)),
    }
}

fn render_text(receipt: &EvaluationReceipt) -> String {
    let mut out = String::new();
    out.push_str("gha-expression-proof receipt\n");
    out.push_str(&format!("mode: {}\n", receipt.mode));
    if let Some(expression) = &receipt.expression {
        out.push_str(&format!("expression: {expression}\n"));
    }
    if let Some(result) = &receipt.result_string {
        out.push_str(&format!("result: {result}\n"));
    }
    if let Some(rendered) = &receipt.rendered {
        out.push_str(&format!("rendered: {rendered}\n"));
    }
    out.push_str(&format!(
        "summary: {} passed, {} warnings, {} failed, {} skipped\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    out.push_str("\nchecks:\n");
    for check in &receipt.checks {
        out.push_str(&format!(
            "[{}] {} - {}\n",
            check.status.as_str(),
            check.id,
            check.message
        ));
    }

    out
}

fn render_markdown(receipt: &EvaluationReceipt) -> String {
    let mut out = String::new();
    out.push_str("# gha-expression-proof Receipt\n\n");
    out.push_str(&format!("- Mode: `{}`\n", receipt.mode));
    if let Some(expression) = &receipt.expression {
        out.push_str(&format!("- Expression: `{}`\n", escape_pipe(expression)));
    }
    if let Some(result) = &receipt.result_string {
        out.push_str(&format!("- Result: `{}`\n", escape_pipe(result)));
    }
    if let Some(rendered) = &receipt.rendered {
        out.push_str(&format!("- Rendered: `{}`\n", escape_pipe(rendered)));
    }
    out.push_str(&format!(
        "- Summary: {} passed, {} warnings, {} failed, {} skipped\n",
        receipt.summary.passed,
        receipt.summary.warnings,
        receipt.summary.failed,
        receipt.summary.skipped
    ));

    out.push_str("\n## Checks\n\n");
    out.push_str("| Status | ID | Message |\n| --- | --- | --- |\n");
    for check in &receipt.checks {
        out.push_str(&format!(
            "| `{}` | `{}` | {} |\n",
            label(check.status),
            check.id,
            escape_pipe(&check.message)
        ));
    }
    out
}

fn label(status: CheckStatus) -> &'static str {
    status.as_str()
}

fn escape_pipe(value: &str) -> String {
    value.replace('|', "\\|").replace('\n', " ")
}