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\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{}\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.selector_precision,
60            row.broad_scope,
61            row.review_after.unwrap_or("-"),
62            row.expires.unwrap_or("-"),
63            row.reason
64        ));
65    }
66    if rows.is_empty() {
67        out.push_str("(no allow entries matched filters)\n");
68    }
69    out.push_str(CLAIM_BOUNDARY_TEXT);
70    out.push('\n');
71    out
72}
73
74fn list_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
75    inventory
76        .files_scanned
77        .map(|files| format!("; files scanned: {files}"))
78        .unwrap_or_default()
79}
80
81fn empty_as_dash(value: &str) -> &str {
82    if value.trim().is_empty() { "-" } else { value }
83}
84
85fn render_list_row_json(row: &ListRow<'_>) -> String {
86    let mut out = String::new();
87    out.push_str("    {\n");
88    out.push_str(&format!("      \"id\": \"{}\",\n", json_escape(row.id)));
89    out.push_str(&format!(
90        "      \"status\": \"{}\",\n",
91        json_escape(row.status)
92    ));
93    out.push_str(&format!("      \"matches\": {},\n", row.matches));
94    out.push_str(&format!("      \"kind\": \"{}\",\n", json_escape(row.kind)));
95    out.push_str(&format!("      \"family\": {},\n", option_json(row.family)));
96    out.push_str(&format!(
97        "      \"owner\": \"{}\",\n",
98        json_escape(row.owner)
99    ));
100    out.push_str(&format!(
101        "      \"classification\": \"{}\",\n",
102        json_escape(row.classification)
103    ));
104    out.push_str(&format!(
105        "      \"scope\": \"{}\",\n",
106        json_escape(row.scope)
107    ));
108    out.push_str(&format!(
109        "      \"source_package\": {},\n",
110        option_json(row.source_package)
111    ));
112    out.push_str(&format!(
113        "      \"evidence_count\": {},\n",
114        row.evidence_count
115    ));
116    out.push_str(&format!(
117        "      \"selector_precision\": {},\n",
118        row.selector_precision
119    ));
120    out.push_str(&format!(
121        "      \"broad_scope\": {},\n",
122        bool_json(row.broad_scope)
123    ));
124    out.push_str(&format!(
125        "      \"review_after\": {},\n",
126        option_json(row.review_after)
127    ));
128    out.push_str(&format!(
129        "      \"expires\": {},\n",
130        option_json(row.expires)
131    ));
132    out.push_str(&format!(
133        "      \"reason\": \"{}\"\n",
134        json_escape(row.reason)
135    ));
136    out.push_str("    }");
137    out
138}
139
140fn render_list_filters_json(filters: ListFilters<'_>, indent: &str) -> String {
141    let mut out = String::new();
142    out.push_str("{\n");
143    out.push_str(&format!(
144        "{indent}  \"kind\": {},\n",
145        option_json(filters.kind)
146    ));
147    out.push_str(&format!(
148        "{indent}  \"family\": {},\n",
149        option_json(filters.family)
150    ));
151    out.push_str(&format!(
152        "{indent}  \"owner\": {},\n",
153        option_json(filters.owner)
154    ));
155    out.push_str(&format!(
156        "{indent}  \"classification\": {},\n",
157        option_json(filters.classification)
158    ));
159    out.push_str(&format!(
160        "{indent}  \"path\": {},\n",
161        option_json(filters.path)
162    ));
163    out.push_str(&format!(
164        "{indent}  \"source_package\": {},\n",
165        option_json(filters.source_package)
166    ));
167    out.push_str(&format!(
168        "{indent}  \"allow_id\": {},\n",
169        option_json(filters.allow_id)
170    ));
171    out.push_str(&format!(
172        "{indent}  \"status\": {},\n",
173        option_json(filters.status)
174    ));
175    out.push_str(&format!(
176        "{indent}  \"expired\": {},\n",
177        bool_json(filters.expired)
178    ));
179    out.push_str(&format!(
180        "{indent}  \"review_due\": {},\n",
181        bool_json(filters.review_due)
182    ));
183    out.push_str(&format!(
184        "{indent}  \"stale\": {},\n",
185        bool_json(filters.stale)
186    ));
187    out.push_str(&format!(
188        "{indent}  \"baseline_debt\": {},\n",
189        bool_json(filters.baseline_debt)
190    ));
191    out.push_str(&format!(
192        "{indent}  \"broad_scope\": {},\n",
193        bool_json(filters.broad_scope)
194    ));
195    out.push_str(&format!(
196        "{indent}  \"missing_evidence\": {}\n",
197        bool_json(filters.missing_evidence)
198    ));
199    out.push_str(&format!("{indent}}}"));
200    out
201}