cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use clap::ArgMatches;

use crate::domain::model::check::{CheckViolation, Severity};
use crate::domain::usecases::check::{
    run_check, run_fix, CheckReport, CheckedEntry, DrSource, FixDrSource, FixIssueSource, FixMode,
    FixReport, IssueSource,
};
use crate::infra::driving::cli::check_renderer::{render, render_with_hint};

use super::super::errors::{die1, CliError};
use super::super::theme;
use super::super::Context;

// ── Serialisable check result ─────────────────────────────────────────────────

#[derive(serde::Serialize)]
pub(in super::super) struct CheckEntry {
    pub kind: String,
    pub path: String,
    pub ok: bool,
    pub violations: Vec<CheckViolationView>,
}

#[derive(serde::Serialize)]
pub(in super::super) struct CheckViolationView {
    #[serde(with = "crate::infra::serde_support::severity")]
    pub severity: Severity,
    pub message: String,
}

impl From<&CheckViolation> for CheckViolationView {
    fn from(v: &CheckViolation) -> Self {
        CheckViolationView {
            severity: v.severity,
            message: render(&v.kind),
        }
    }
}

impl From<&CheckedEntry> for CheckEntry {
    fn from(e: &CheckedEntry) -> Self {
        CheckEntry {
            kind: e.kind.clone(),
            path: e.path.display().to_string(),
            ok: !e.violations.has_errors(),
            violations: e.violations.iter().map(CheckViolationView::from).collect(),
        }
    }
}

// ── Global check ──────────────────────────────────────────────────────────────

pub(in super::super) fn execute_global_check(matches: &ArgMatches, ctx: &Context<'_>) {
    let config = ctx.config();
    let output_fmt = ctx.output_fmt;
    let verbose = matches.get_flag("verbose");
    let fix = matches.get_flag("fix");
    let dry_run = matches.get_flag("dry-run");

    let known_refs = ctx.load_known_refs().unwrap_or_else(|e| {
        die1(
            CliError::new(format!("loading workspace refs: {e}")),
            output_fmt,
        );
    });

    let issue_repo = ctx.issue_repository();
    let issue_defects = ctx.issue_defect_scanner();
    let dr_repos: Vec<_> = config
        .decision_kinds
        .iter()
        .map(|k| (k.kind.clone(), ctx.decision_record_repository(k)))
        .collect();
    let dr_defects: Vec<_> = config
        .decision_kinds
        .iter()
        .map(|k| ctx.decision_record_defect_scanner(k))
        .collect();
    let dr_descriptors: Vec<_> = config
        .decision_kinds
        .iter()
        .map(|k| config.tag_descriptors_for(&k.kind))
        .collect();
    let issue_descriptors = config.tag_descriptors_for("issues");

    if fix {
        let fix_dr_sources: Vec<FixDrSource<'_>> = dr_repos
            .iter()
            .zip(dr_descriptors.iter())
            .map(|((k, r), td)| FixDrSource {
                kind: k.as_str(),
                repo: r,
                tag_descriptors: td,
            })
            .collect();
        let mode = if dry_run {
            FixMode::DryRun
        } else {
            FixMode::Apply
        };
        let report = run_fix(
            FixIssueSource {
                repo: &issue_repo,
                statuses: ctx.issues_statuses,
                tag_descriptors: &issue_descriptors,
            },
            &fix_dr_sources,
            &known_refs,
            mode,
        )
        .unwrap_or_else(|e| {
            die1(CliError::new(format!("fix: {e}")), output_fmt);
        });
        render_fix(&report, mode);
    }

    let dr_sources: Vec<DrSource<'_>> = dr_repos
        .iter()
        .zip(dr_defects.iter())
        .zip(dr_descriptors.iter())
        .map(|(((k, r), d), td)| DrSource {
            kind: k.as_str(),
            repo: r,
            defect_scanner: d,
            tag_descriptors: td,
        })
        .collect();
    let report = run_check(
        Some(IssueSource {
            repo: &issue_repo,
            defect_scanner: &issue_defects,
            content_reader: &issue_repo,
            statuses: ctx.issues_statuses,
            tag_descriptors: &issue_descriptors,
        }),
        &dr_sources,
        &known_refs,
    )
    .unwrap_or_else(|e| {
        die1(CliError::new(format!("check: {e}")), output_fmt);
    });

    render_schema_warning(ctx);

    if output_fmt.is_structured() {
        let entries: Vec<CheckEntry> = report.entries.iter().map(CheckEntry::from).collect();
        super::super::render_structured(&entries, output_fmt);
    } else {
        render_human(&report, verbose);
    }

    if report.has_errors() {
        std::process::exit(1);
    }
}

fn render_schema_warning(ctx: &Context<'_>) {
    use crate::infra::driven::fs::config::CURRENT_SCHEMA_VERSION;
    let config = ctx.config();
    if config.schema_version < CURRENT_SCHEMA_VERSION && !ctx.output_fmt.is_structured() {
        println!(
            "{}",
            theme::check_warn(
                "[config]",
                &format!(
                    "cartulary.toml has no version field (schema version 0). \
                    Run 'cartu migrate' to upgrade to version {CURRENT_SCHEMA_VERSION}."
                )
            )
        );
    }
}

fn render_fix(report: &FixReport, mode: FixMode) {
    let verb = if matches!(mode, FixMode::DryRun) {
        "would fix"
    } else {
        "fixed"
    };
    for item in &report.items {
        println!("{verb} [{}]: {}", item.rule_id, item.description);
    }
}

fn render_human(report: &CheckReport, verbose: bool) {
    render_human_entries(&report.entries, verbose);
}

pub(in super::super) fn render_human_entries(entries: &[CheckedEntry], verbose: bool) {
    for entry in entries {
        if entry.violations.is_empty() {
            if verbose {
                let path = format!("[{}] {}", entry.kind, entry.path.display());
                println!("{}", theme::check_ok(&path));
            }
            continue;
        }
        for violation in &entry.violations {
            let path = format!("[{}] {}", entry.kind, entry.path.display());
            match violation.severity {
                Severity::Error => {
                    println!(
                        "{}",
                        theme::check_error(&path, &render_with_hint(violation))
                    );
                }
                Severity::Warning => {
                    println!("{}", theme::check_warn(&path, &render_with_hint(violation)));
                }
            }
        }
    }
}