cargo-reclaim 0.2.2

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::io::Write;

use cargo_reclaim::{
    CargoHomeApplyEntryStatus, CargoHomeApplyReport, CargoHomeEntry, CargoHomePlan,
    CargoHomePlanEntry, CargoHomeReport,
};
use serde::Serialize;

use super::super::CliError;
use super::labels::{
    action_label, class_label, path_kind_label, path_string, policy_label, source_label,
};

pub(super) fn write_json_report(
    output: &mut impl Write,
    report: &CargoHomeReport,
) -> Result<(), CliError> {
    let document = JsonCargoHomeReport::from_report(report);
    serde_json::to_writer(&mut *output, &document)?;
    writeln!(output)?;
    Ok(())
}

pub(super) fn write_json_plan(
    output: &mut impl Write,
    plan: &CargoHomePlan,
) -> Result<(), CliError> {
    let document = JsonCargoHomePlan::from_plan(plan);
    serde_json::to_writer(&mut *output, &document)?;
    writeln!(output)?;
    Ok(())
}

pub(super) fn write_json_apply_report(
    output: &mut impl Write,
    report: &CargoHomeApplyReport,
) -> Result<(), CliError> {
    let entries = report
        .entries
        .iter()
        .map(|entry| {
            serde_json::json!({
                "path": entry.path,
                "planned_action": entry.planned_action,
                "status": apply_status_label(entry.status),
                "size_bytes": entry.size_bytes,
                "reason": entry.reason,
            })
        })
        .collect::<Vec<_>>();
    let document = serde_json::json!({
        "schema_version": 1,
        "command": "cargo-home apply",
        "dry_run": report.dry_run,
        "validation_only": report.validation_only,
        "plan_id": report.plan_id.as_str(),
        "totals": {
            "entry_count": report.totals.entry_count,
            "delete_candidate_count": report.totals.delete_candidate_count,
            "would_delete_count": report.totals.would_delete_count,
            "applied_count": report.totals.applied_count,
            "failed_count": report.totals.failed_count,
            "skipped_count": report.totals.skipped_count,
            "stale_skip_count": report.totals.stale_skip_count,
            "would_delete_bytes": report.totals.would_delete_bytes,
            "applied_bytes": report.totals.applied_bytes,
        },
        "entries": entries,
    });
    serde_json::to_writer(&mut *output, &document)?;
    writeln!(output)?;
    Ok(())
}

#[derive(Serialize)]
struct JsonCargoHomeReport {
    schema_version: u16,
    command: &'static str,
    dry_run: bool,
    input: JsonCargoHomeInput,
    totals: JsonCargoHomeTotals,
    entries: Vec<JsonCargoHomeEntry>,
    recommendations: Vec<String>,
    problems: Vec<JsonCargoHomeProblem>,
}

impl JsonCargoHomeReport {
    fn from_report(report: &CargoHomeReport) -> Self {
        Self {
            schema_version: report.schema_version,
            command: "cargo-home report",
            dry_run: true,
            input: JsonCargoHomeInput {
                root: path_string(&report.input.root),
                source: source_label(report.input.source),
            },
            totals: JsonCargoHomeTotals {
                entry_count: report.totals.entry_count,
                total_bytes: report.totals.total_bytes,
                cache_bytes: report.totals.cache_bytes,
                preserved_bytes: report.totals.preserved_bytes,
                skipped_count: report.totals.skipped_count,
                problem_count: report.totals.problem_count,
                known_cache_entry_count: report.totals.known_cache_entry_count,
            },
            entries: report
                .entries
                .iter()
                .map(JsonCargoHomeEntry::from_entry)
                .collect(),
            recommendations: report
                .recommendations
                .iter()
                .map(|recommendation| recommendation.message.clone())
                .collect(),
            problems: report
                .problems
                .iter()
                .map(|problem| JsonCargoHomeProblem {
                    path: path_string(&problem.path),
                    message: problem.message.clone(),
                })
                .collect(),
        }
    }
}

#[derive(Serialize)]
struct JsonCargoHomeInput {
    root: String,
    source: &'static str,
}

#[derive(Serialize)]
struct JsonCargoHomeTotals {
    entry_count: usize,
    total_bytes: u64,
    cache_bytes: u64,
    preserved_bytes: u64,
    skipped_count: usize,
    problem_count: usize,
    known_cache_entry_count: usize,
}

#[derive(Serialize)]
struct JsonCargoHomeEntry {
    path: String,
    relative_path: String,
    class: &'static str,
    path_kind: &'static str,
    size_bytes: u64,
    preserved: bool,
    skipped: bool,
    reason: String,
}

impl JsonCargoHomeEntry {
    fn from_entry(entry: &CargoHomeEntry) -> Self {
        Self {
            path: path_string(&entry.path),
            relative_path: path_string(&entry.relative_path),
            class: class_label(entry.class),
            path_kind: path_kind_label(entry.path_kind),
            size_bytes: entry.size_bytes,
            preserved: entry.preserved,
            skipped: entry.skipped,
            reason: entry.reason.clone(),
        }
    }
}

#[derive(Serialize)]
struct JsonCargoHomeProblem {
    path: String,
    message: String,
}

#[derive(Serialize)]
struct JsonCargoHomePlan {
    schema_version: u16,
    command: &'static str,
    dry_run: bool,
    policy: &'static str,
    input: JsonCargoHomeInput,
    totals: JsonCargoHomePlanTotals,
    entries: Vec<JsonCargoHomePlanEntry>,
    recommendations: Vec<String>,
    problems: Vec<JsonCargoHomeProblem>,
}

impl JsonCargoHomePlan {
    fn from_plan(plan: &CargoHomePlan) -> Self {
        Self {
            schema_version: plan.schema_version,
            command: "cargo-home plan",
            dry_run: true,
            policy: policy_label(plan.policy),
            input: JsonCargoHomeInput {
                root: path_string(&plan.input.root),
                source: source_label(plan.input.source),
            },
            totals: JsonCargoHomePlanTotals {
                entry_count: plan.totals.entry_count,
                total_bytes: plan.totals.total_bytes,
                delete_candidate_count: plan.totals.delete_candidate_count,
                delete_candidate_bytes: plan.totals.delete_candidate_bytes,
                preserved_count: plan.totals.preserved_count,
                preserved_bytes: plan.totals.preserved_bytes,
                skipped_count: plan.totals.skipped_count,
                problem_count: plan.totals.problem_count,
            },
            entries: plan
                .entries
                .iter()
                .map(JsonCargoHomePlanEntry::from_entry)
                .collect(),
            recommendations: plan
                .recommendations
                .iter()
                .map(|recommendation| recommendation.message.clone())
                .collect(),
            problems: plan
                .problems
                .iter()
                .map(|problem| JsonCargoHomeProblem {
                    path: path_string(&problem.path),
                    message: problem.message.clone(),
                })
                .collect(),
        }
    }
}

#[derive(Serialize)]
struct JsonCargoHomePlanTotals {
    entry_count: usize,
    total_bytes: u64,
    delete_candidate_count: usize,
    delete_candidate_bytes: u64,
    preserved_count: usize,
    preserved_bytes: u64,
    skipped_count: usize,
    problem_count: usize,
}

#[derive(Serialize)]
struct JsonCargoHomePlanEntry {
    path: String,
    relative_path: String,
    class: &'static str,
    path_kind: &'static str,
    size_bytes: u64,
    action: &'static str,
    reason: String,
}

impl JsonCargoHomePlanEntry {
    fn from_entry(entry: &CargoHomePlanEntry) -> Self {
        Self {
            path: path_string(&entry.path),
            relative_path: path_string(&entry.relative_path),
            class: class_label(entry.class),
            path_kind: path_kind_label(entry.path_kind),
            size_bytes: entry.size_bytes,
            action: action_label(entry.action),
            reason: entry.reason.clone(),
        }
    }
}

fn apply_status_label(status: CargoHomeApplyEntryStatus) -> &'static str {
    match status {
        CargoHomeApplyEntryStatus::WouldDelete => "would_delete",
        CargoHomeApplyEntryStatus::Deleted => "deleted",
        CargoHomeApplyEntryStatus::NotPlannedForDeletion => "not_planned_for_deletion",
        CargoHomeApplyEntryStatus::SkipStalePlan => "skip_stale_plan",
        CargoHomeApplyEntryStatus::DeleteFailed => "delete_failed",
    }
}