Skip to main content

allow_report/
list.rs

1use crate::contracts::LIST_ARTIFACT;
2use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
3use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, ListFilters, ListRow};
4use allow_core::json_escape;
5
6pub fn render_list_json(
7    rows: &[ListRow<'_>],
8    filters: ListFilters<'_>,
9    inventory: InventoryContext<'_>,
10) -> String {
11    let mut out = String::new();
12    out.push_str("{\n");
13    push_json_fixed_artifact_preamble(&mut out, LIST_ARTIFACT, inventory);
14    out.push_str("  \"filters\": ");
15    out.push_str(&render_list_filters_json(filters, "  "));
16    out.push_str(",\n");
17    out.push_str(&format!(
18        "  \"summary\": {{\n    \"allow_entries\": {}\n  }},\n",
19        rows.len()
20    ));
21    out.push_str("  \"allow_entries\": [\n");
22    for (index, row) in rows.iter().enumerate() {
23        if index > 0 {
24            out.push_str(",\n");
25        }
26        out.push_str(&render_list_row_json(row));
27    }
28    out.push_str("\n  ]\n");
29    out.push_str("}\n");
30    out
31}
32
33pub fn render_list_human(rows: &[ListRow<'_>], inventory: InventoryContext<'_>) -> String {
34    let mut out = String::new();
35    out.push_str(&format!(
36        "inventory: {}/{} via {}{}\n",
37        inventory.scope,
38        inventory.scanner,
39        inventory.source,
40        list_inventory_files_suffix(inventory)
41    ));
42    if let Some(root) = inventory.root {
43        out.push_str(&format!("source_tree_root: {root}\n"));
44    }
45    out.push_str("id\tstatus\tmatches\tkind\tfamily\towner\tclassification\tscope\tsource_package\tevidence_count\tbroken_evidence_references\tweak_evidence_references\tselector_precision\tbroad_scope\treview_after\texpires\treason\n");
46    for row in rows {
47        out.push_str(&format!(
48            "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
49            row.id,
50            row.status,
51            row.matches,
52            row.kind,
53            row.family.unwrap_or("-"),
54            empty_as_dash(row.owner),
55            empty_as_dash(row.classification),
56            row.scope,
57            row.source_package.unwrap_or("-"),
58            row.evidence_count,
59            row.broken_evidence_references,
60            row.weak_evidence_references,
61            row.selector_precision,
62            row.broad_scope,
63            row.review_after.unwrap_or("-"),
64            row.expires.unwrap_or("-"),
65            row.reason
66        ));
67    }
68    if rows.is_empty() {
69        out.push_str("(no allow entries matched filters)\n");
70    }
71    out.push_str(CLAIM_BOUNDARY_TEXT);
72    out.push('\n');
73    out
74}
75
76fn list_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
77    inventory
78        .files_scanned
79        .map(|files| format!("; files scanned: {files}"))
80        .unwrap_or_default()
81}
82
83fn empty_as_dash(value: &str) -> &str {
84    if value.trim().is_empty() { "-" } else { value }
85}
86
87fn render_list_row_json(row: &ListRow<'_>) -> String {
88    let mut out = String::new();
89    out.push_str("    {\n");
90    out.push_str(&format!("      \"id\": \"{}\",\n", json_escape(row.id)));
91    out.push_str(&format!(
92        "      \"status\": \"{}\",\n",
93        json_escape(row.status)
94    ));
95    out.push_str(&format!("      \"matches\": {},\n", row.matches));
96    out.push_str(&format!("      \"kind\": \"{}\",\n", json_escape(row.kind)));
97    out.push_str(&format!("      \"family\": {},\n", option_json(row.family)));
98    out.push_str(&format!(
99        "      \"owner\": \"{}\",\n",
100        json_escape(row.owner)
101    ));
102    out.push_str(&format!(
103        "      \"classification\": \"{}\",\n",
104        json_escape(row.classification)
105    ));
106    out.push_str(&format!(
107        "      \"scope\": \"{}\",\n",
108        json_escape(row.scope)
109    ));
110    out.push_str(&format!(
111        "      \"source_package\": {},\n",
112        option_json(row.source_package)
113    ));
114    out.push_str(&format!(
115        "      \"evidence_count\": {},\n",
116        row.evidence_count
117    ));
118    if row.broken_evidence_references > 0 {
119        out.push_str(&format!(
120            "      \"broken_evidence_references\": {},\n",
121            row.broken_evidence_references
122        ));
123    }
124    if row.weak_evidence_references > 0 {
125        out.push_str(&format!(
126            "      \"weak_evidence_references\": {},\n",
127            row.weak_evidence_references
128        ));
129    }
130    out.push_str(&format!(
131        "      \"selector_precision\": {},\n",
132        row.selector_precision
133    ));
134    out.push_str(&format!(
135        "      \"broad_scope\": {},\n",
136        bool_json(row.broad_scope)
137    ));
138    out.push_str(&format!(
139        "      \"review_after\": {},\n",
140        option_json(row.review_after)
141    ));
142    out.push_str(&format!(
143        "      \"expires\": {},\n",
144        option_json(row.expires)
145    ));
146    out.push_str(&format!(
147        "      \"reason\": \"{}\"\n",
148        json_escape(row.reason)
149    ));
150    out.push_str("    }");
151    out
152}
153
154fn render_list_filters_json(filters: ListFilters<'_>, indent: &str) -> String {
155    let mut out = String::new();
156    out.push_str("{\n");
157    out.push_str(&format!(
158        "{indent}  \"kind\": {},\n",
159        option_json(filters.kind)
160    ));
161    out.push_str(&format!(
162        "{indent}  \"family\": {},\n",
163        option_json(filters.family)
164    ));
165    out.push_str(&format!(
166        "{indent}  \"owner\": {},\n",
167        option_json(filters.owner)
168    ));
169    out.push_str(&format!(
170        "{indent}  \"classification\": {},\n",
171        option_json(filters.classification)
172    ));
173    out.push_str(&format!(
174        "{indent}  \"path\": {},\n",
175        option_json(filters.path)
176    ));
177    out.push_str(&format!(
178        "{indent}  \"source_package\": {},\n",
179        option_json(filters.source_package)
180    ));
181    out.push_str(&format!(
182        "{indent}  \"allow_id\": {},\n",
183        option_json(filters.allow_id)
184    ));
185    out.push_str(&format!(
186        "{indent}  \"status\": {},\n",
187        option_json(filters.status)
188    ));
189    out.push_str(&format!(
190        "{indent}  \"expired\": {},\n",
191        bool_json(filters.expired)
192    ));
193    out.push_str(&format!(
194        "{indent}  \"review_due\": {},\n",
195        bool_json(filters.review_due)
196    ));
197    out.push_str(&format!(
198        "{indent}  \"stale\": {},\n",
199        bool_json(filters.stale)
200    ));
201    out.push_str(&format!(
202        "{indent}  \"baseline_debt\": {},\n",
203        bool_json(filters.baseline_debt)
204    ));
205    out.push_str(&format!(
206        "{indent}  \"broad_scope\": {},\n",
207        bool_json(filters.broad_scope)
208    ));
209    out.push_str(&format!(
210        "{indent}  \"missing_evidence\": {},\n",
211        bool_json(filters.missing_evidence)
212    ));
213    out.push_str(&format!(
214        "{indent}  \"broken_evidence\": {},\n",
215        bool_json(filters.broken_evidence)
216    ));
217    out.push_str(&format!(
218        "{indent}  \"weak_evidence\": {}\n",
219        bool_json(filters.weak_evidence)
220    ));
221    out.push_str(&format!("{indent}}}"));
222    out
223}