1use crate::contracts::PRUNE_ARTIFACT;
2use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
3use crate::text::markdown_cell;
4use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, PruneCandidate, PruneModeContext};
5use allow_core::json_escape;
6
7pub fn render_prune_human(candidates: &[PruneCandidate<'_>], mode: PruneModeContext<'_>) -> String {
8 render_prune_human_with_context(candidates, mode, InventoryContext::unknown_source_syntax())
9}
10
11pub fn render_prune_human_with_context(
12 candidates: &[PruneCandidate<'_>],
13 mode: PruneModeContext<'_>,
14 inventory: InventoryContext<'_>,
15) -> String {
16 let mut out = String::new();
17 out.push_str("cargo-allow prune\n\n");
18 out.push_str(&format!(
19 "Inventory: {}/{} via {}{}\n",
20 inventory.scope,
21 inventory.scanner,
22 inventory.source,
23 prune_inventory_files_suffix(inventory)
24 ));
25 if let Some(root) = inventory.root {
26 out.push_str(&format!("Source tree root: {root}\n"));
27 }
28 if mode.write_requested {
29 out.push_str("mode: write\n");
30 } else {
31 out.push_str("mode: dry-run\n");
32 }
33 if mode.explicit_dry_run {
34 out.push_str("requested: --dry-run\n");
35 }
36 out.push_str(&format!("stale entries: {}\n\n", candidates.len()));
37 if candidates.is_empty() {
38 out.push_str("No stale allow entries found.\n");
39 out.push('\n');
40 out.push_str(CLAIM_BOUNDARY_TEXT);
41 out.push('\n');
42 return out;
43 }
44 out.push_str("| Allow ID | Kind | Family | Owner | Classification | Scope | Reason |\n");
45 out.push_str("|---|---|---|---|---|---|---|\n");
46 for candidate in candidates {
47 out.push_str(&format!(
48 "| `{}` | `{}` | `{}` | `{}` | `{}` | `{}` | {} |\n",
49 markdown_cell(candidate.id),
50 candidate.kind,
51 markdown_cell(candidate.family.unwrap_or("-")),
52 markdown_cell(candidate.owner),
53 markdown_cell(candidate.classification),
54 markdown_cell(candidate.scope),
55 markdown_cell(candidate.reason)
56 ));
57 }
58 if let Some(path) = mode.written_path {
59 out.push_str(&format!(
60 "\nRemoved stale entries from `{}`.\n",
61 markdown_cell(path)
62 ));
63 } else {
64 out.push_str(
65 "\nNo files were changed. Remove these entries only after confirming the exception is gone.\n",
66 );
67 }
68 out.push('\n');
69 out.push_str(CLAIM_BOUNDARY_TEXT);
70 out.push('\n');
71 out
72}
73
74fn prune_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
75 inventory
76 .files_scanned
77 .map(|files| format!("; files scanned: {files}"))
78 .unwrap_or_default()
79}
80
81pub fn render_prune_json(
82 candidates: &[PruneCandidate<'_>],
83 mode: PruneModeContext<'_>,
84 inventory: InventoryContext<'_>,
85) -> String {
86 let mut out = String::new();
87 out.push_str("{\n");
88 push_json_fixed_artifact_preamble(&mut out, PRUNE_ARTIFACT, inventory);
89 out.push_str(" \"mode\": {\n");
90 out.push_str(&format!(
91 " \"dry_run\": {},\n",
92 bool_json(!mode.write_requested)
93 ));
94 out.push_str(&format!(
95 " \"write_requested\": {},\n",
96 bool_json(mode.write_requested)
97 ));
98 out.push_str(&format!(
99 " \"explicit_dry_run\": {},\n",
100 bool_json(mode.explicit_dry_run)
101 ));
102 out.push_str(&format!(
103 " \"written_path\": {}\n",
104 option_json(mode.written_path)
105 ));
106 out.push_str(" },\n");
107 out.push_str(&format!(
108 " \"summary\": {{\n \"stale_entries\": {}\n }},\n",
109 candidates.len()
110 ));
111 out.push_str(" \"stale_entries\": [\n");
112 for (index, candidate) in candidates.iter().enumerate() {
113 if index > 0 {
114 out.push_str(",\n");
115 }
116 out.push_str(&render_prune_candidate_json(candidate, " "));
117 }
118 out.push_str("\n ]\n");
119 out.push_str("}\n");
120 out
121}
122
123fn render_prune_candidate_json(candidate: &PruneCandidate<'_>, indent: &str) -> String {
124 format!(
125 "{indent} {{\n{indent} \"id\": \"{}\",\n{indent} \"kind\": \"{}\",\n{indent} \"family\": {},\n{indent} \"owner\": \"{}\",\n{indent} \"classification\": \"{}\",\n{indent} \"scope\": \"{}\",\n{indent} \"reason\": \"{}\"\n{indent} }}",
126 json_escape(candidate.id),
127 json_escape(candidate.kind),
128 option_json(candidate.family),
129 json_escape(candidate.owner),
130 json_escape(candidate.classification),
131 json_escape(candidate.scope),
132 json_escape(candidate.reason)
133 )
134}